Skip to content

Commit

Permalink
- adds error mapping as parameter to http request adapter dotnet
Browse files Browse the repository at this point in the history
- adds errors deserialization and throw in dotnet

Signed-off-by: Vincent Biret <vibiret@microsoft.com>
  • Loading branch information
baywet committed Feb 3, 2022
1 parent 33f88fb commit ade2fd1
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 10 deletions.
16 changes: 11 additions & 5 deletions abstractions/dotnet/src/IRequestAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -29,41 +30,46 @@ public interface IRequestAdapter
/// </summary>
/// <param name="requestInfo">The RequestInformation object to use for the HTTP request.</param>
/// <param name="responseHandler">The response handler to use for the HTTP request instead of the default handler.</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the requests.</param>
/// <returns>The deserialized response model.</returns>
Task<ModelType> SendAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) where ModelType : IParsable;
Task<ModelType> SendAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary<string, Func<IParsable>> errorMapping = default, CancellationToken cancellationToken = default) where ModelType : IParsable;
/// <summary>
/// Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection.
/// </summary>
/// <param name="requestInfo">The RequestInformation object to use for the HTTP request.</param>
/// <param name="responseHandler">The response handler to use for the HTTP request instead of the default handler.</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the requests.</param>
/// <returns>The deserialized response model collection.</returns>
Task<IEnumerable<ModelType>> SendCollectionAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) where ModelType : IParsable;
Task<IEnumerable<ModelType>> SendCollectionAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary<string, Func<IParsable>> errorMapping = default, CancellationToken cancellationToken = default) where ModelType : IParsable;
/// <summary>
/// Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model.
/// </summary>
/// <param name="requestInfo">The RequestInformation object to use for the HTTP request.</param>
/// <param name="responseHandler">The response handler to use for the HTTP request instead of the default handler.</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the requests.</param>
/// <returns>The deserialized primitive response model.</returns>
Task<ModelType> SendPrimitiveAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default);
Task<ModelType> SendPrimitiveAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary<string, Func<IParsable>> errorMapping = default, CancellationToken cancellationToken = default);
/// <summary>
/// Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection.
/// </summary>
/// <param name="requestInfo">The RequestInformation object to use for the HTTP request.</param>
/// <param name="responseHandler">The response handler to use for the HTTP request instead of the default handler.</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the requests.</param>
/// <returns>The deserialized primitive response model collection.</returns>
Task<IEnumerable<ModelType>> SendPrimitiveCollectionAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default);
Task<IEnumerable<ModelType>> SendPrimitiveCollectionAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary<string, Func<IParsable>> errorMapping = default, CancellationToken cancellationToken = default);
/// <summary>
/// Executes the HTTP request specified by the given RequestInformation with no return content.
/// </summary>
/// <param name="requestInfo">The RequestInformation object to use for the HTTP request.</param>
/// <param name="responseHandler">The response handler to use for the HTTP request instead of the default handler.</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the requests.</param>
/// <returns>A Task to await completion.</returns>
Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default);
Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary<string, Func<IParsable>> errorMapping = default, CancellationToken cancellationToken = default);
/// <summary>
/// The base url for every request.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions abstractions/dotnet/src/serialization/IParseNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ public interface IParseNode
/// <returns>The model object value of the node.</returns>
T GetObjectValue<T>() where T : IParsable;
/// <summary>
/// Gets the resulting error from the node.
/// </summary>
/// <returns>The error object value of the node.</returns>
/// <param name="factory">The error factory.</param>
IParsable GetErrorValue(Func<IParsable> factory);
/// <summary>
/// Callback called before the node is deserialized.
/// </summary>
Action<IParsable> OnBeforeAssignFieldValues { get; set; }
Expand Down
38 changes: 33 additions & 5 deletions http/dotnet/httpclient/src/HttpClientRequestAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@ public ISerializationWriterFactory SerializationWriterFactory
/// </summary>
/// <param name="requestInfo">The <see cref="RequestInformation"/> instance to send</param>
/// <param name="responseHandler">The <see cref="IResponseHandler"/> to use with the response</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the request.</param>
public async Task<IEnumerable<ModelType>> SendCollectionAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) where ModelType : IParsable
public async Task<IEnumerable<ModelType>> SendCollectionAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary<string, Func<IParsable>> 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<ModelType>();
return result;
Expand All @@ -77,13 +79,15 @@ public async Task<IEnumerable<ModelType>> SendCollectionAsync<ModelType>(Request
/// </summary>
/// <param name="requestInfo">The RequestInformation object to use for the HTTP request.</param>
/// <param name="responseHandler">The response handler to use for the HTTP request instead of the default handler.</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the request.</param>
/// <returns>The deserialized primitive response model collection.</returns>
public async Task<IEnumerable<ModelType>> SendPrimitiveCollectionAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) {
public async Task<IEnumerable<ModelType>> SendPrimitiveCollectionAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary<string, Func<IParsable>> 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<ModelType>();
return result;
Expand All @@ -96,14 +100,16 @@ public async Task<IEnumerable<ModelType>> SendPrimitiveCollectionAsync<ModelType
/// </summary>
/// <param name="requestInfo">The <see cref="RequestInformation"/> instance to send</param>
/// <param name="responseHandler">The <see cref="IResponseHandler"/> to use with the response</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the request.</param>
/// <returns>The deserialized response model.</returns>
public async Task<ModelType> SendAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = null, CancellationToken cancellationToken = default) where ModelType : IParsable
public async Task<ModelType> SendAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = null, Dictionary<string, Func<IParsable>> 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<ModelType>();
return result;
Expand All @@ -116,9 +122,10 @@ public async Task<ModelType> SendAsync<ModelType>(RequestInformation requestInfo
/// </summary>
/// <param name="requestInfo">The <see cref="RequestInformation"/> instance to send</param>
/// <param name="responseHandler">The <see cref="IResponseHandler"/> to use with the response</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the request.</param>
/// <returns>The deserialized primitive response model.</returns>
public async Task<ModelType> SendPrimitiveAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default)
public async Task<ModelType> SendPrimitiveAsync<ModelType>(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary<string, Func<IParsable>> errorMapping = default, CancellationToken cancellationToken = default)
{
var response = await GetHttpResponseMessage(requestInfo, cancellationToken);
requestInfo.Content?.Dispose();
Expand All @@ -131,6 +138,7 @@ public async Task<ModelType> SendPrimitiveAsync<ModelType>(RequestInformation re
}
else
{
await ThrowFailedResponse(response, errorMapping);
var rootNode = await GetRootParseNode(response);
object result;
if(modelType == typeof(bool))
Expand Down Expand Up @@ -173,9 +181,10 @@ public async Task<ModelType> SendPrimitiveAsync<ModelType>(RequestInformation re
/// </summary>
/// <param name="requestInfo">The <see cref="RequestInformation"/> instance to send</param>
/// <param name="responseHandler">The <see cref="IResponseHandler"/> to use with the response</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the request.</param>
/// <returns></returns>
public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, CancellationToken cancellationToken = default)
public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, Dictionary<string, Func<IParsable>> errorMapping = default, CancellationToken cancellationToken = default)
{
var response = await GetHttpResponseMessage(requestInfo, cancellationToken);
requestInfo.Content?.Dispose();
Expand All @@ -184,6 +193,25 @@ public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHa
else
await responseHandler.HandleResponseAsync<HttpResponseMessage, object>(response);
}
private async Task ThrowFailedResponse(HttpResponseMessage response, Dictionary<string, Func<IParsable>> errorMapping)
{
if(response.IsSuccessStatusCode) return;

var statusCodeAsInt = (int)response.StatusCode;
var statusCodeAsString = statusCodeAsInt.ToString();
Func<IParsable> 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<IParseNode> GetRootParseNode(HttpResponseMessage response)
{
var responseContentType = response.Content.Headers?.ContentType?.MediaType?.ToLowerInvariant();
Expand Down
18 changes: 18 additions & 0 deletions serialization/dotnet/json/src/JsonParseNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,30 @@ public IEnumerable<T> GetCollectionOfPrimitiveValues<T>()
public T GetObjectValue<T>() where T : IParsable
{
var item = (T)(typeof(T).GetConstructor(new Type[] { }).Invoke(new object[] { }));
return GetObjectValueInternal(item);
}
private T GetObjectValueInternal<T>(T item) where T : IParsable
{
var fieldDeserializers = item.GetFieldDeserializers<T>();
OnBeforeAssignFieldValues?.Invoke(item);
AssignFieldValues(item, fieldDeserializers);
OnAfterAssignFieldValues?.Invoke(item);
return item;
}
/// <summary>
/// Gets the resulting error from the node.
/// </summary>
/// <returns>The error object value of the node.</returns>
/// <param name="factory">The error factory.</param>
public IParsable GetErrorValue(Func<IParsable> 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>(T item, IDictionary<string, Action<T, IParseNode>> fieldDeserializers) where T : IParsable
{
if(_jsonNode.ValueKind != JsonValueKind.Object) return;
Expand Down

0 comments on commit ade2fd1

Please sign in to comment.