Skip to content

Commit

Permalink
Add serialization helper methods to the Opw.HttpExceptions.AspNetCore…
Browse files Browse the repository at this point in the history
… package #61 (#67)

Add serialization helper methods to the Opw.HttpExceptions.AspNetCore package
  • Loading branch information
petervandenhout authored Dec 11, 2020
1 parent f83ffcf commit b126647
Show file tree
Hide file tree
Showing 14 changed files with 481 additions and 91 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ mvcBuilder.AddHttpExceptions(options =>
Set the ExceptionMapper collection that will be used during mapping. You can override and/or add ExceptionMappers for specific
exception types. The ExceptionMappers are called in order so make sure you add them in the right order.

By default there is one ExceptionMapper configured, that ExceptionMapper catches all exceptions.
By default there is one ExceptionMapper configured, that ExceptionMapper catches all exceptions.

``` csharp
mvcBuilder.AddHttpExceptions(options =>
Expand All @@ -149,6 +149,22 @@ mvcBuilder.AddHttpExceptions(options =>
});
```

### Serialization helper methods
Serialize the HTTP content to ProblemDetails.
``` csharp
ProblemDetails problemDetails = response.Content.ReadAsProblemDetails();
```

Try to get the exception details from the ProblemDetails.
``` csharp
problemDetails.TryGetExceptionDetails(out SerializableException exception);
```

Try to get the errors dictionary from the ProblemDetails.
``` csharp
problemDetails.TryGetErrors(out IDictionary<string, object[]> errors);
```

### Sample project using HttpExceptions middleware
See the `samples/Opw.HttpExceptions.AspNetCore.Sample` project for a sample implementation. This project contains examples on how to use the HttpExceptions middleware.

Expand Down
37 changes: 37 additions & 0 deletions src/Opw.HttpExceptions.AspNetCore/HttpContentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Opw.HttpExceptions.AspNetCore.Serialization;

namespace Opw.HttpExceptions.AspNetCore
{
/// <summary>
/// Provides extension methods for HttpContent.
/// </summary>
public static class HttpContentExtensions
{
/// <summary>
/// Serialize the HTTP content to ProblemDetails as an asynchronous operation.
/// </summary>
/// <param name="content">The HTTP content.</param>
public static ProblemDetails ReadAsProblemDetails(this HttpContent content)
{
_ = content ?? throw new ArgumentNullException(nameof(content));

if (!content.Headers.ContentType.MediaType.Equals("application/problem+json", StringComparison.OrdinalIgnoreCase))
{
throw new SerializationException("HttpContent is not of type \"application/problem+json\".");
}

var str = content.ReadAsStringAsync().Result;
var converter = new ProblemDetailsJsonConverter();
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(str));
var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), new JsonSerializerOptions());

return problemDetails;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public static void ExceptionMapper<TException, TExceptionMapper>(this HttpExcept
{
if (options.ExceptionMapperDescriptors.ContainsKey(typeof(TException)))
{
options.ExceptionMapperDescriptors[typeof(TException)] = new ExceptionMapperDescriptor {
options.ExceptionMapperDescriptors[typeof(TException)] = new ExceptionMapperDescriptor
{
Type = typeof(TExceptionMapper),
Arguments = arguments
};
Expand Down
84 changes: 84 additions & 0 deletions src/Opw.HttpExceptions.AspNetCore/ProblemDetailsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using Microsoft.AspNetCore.Mvc;
using Opw.HttpExceptions.AspNetCore.Serialization;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;

namespace Opw.HttpExceptions.AspNetCore
{
/// <summary>
/// Provides extension methods for ProblemDetails.
/// </summary>
public static class ProblemDetailsExtensions
{
/// <summary>
/// Try to get the exception details from the ProblemDetails.
/// </summary>
/// <param name="problemDetails">ProblemDetails to get the exception from.</param>
/// <param name="exception">When this method returns, the exception from the ProblemDetails, if the ProblemDetails contains an exception; otherwise, null.</param>
/// <returns>true if the ProblemDetails contains an exception; otherwise, false.</returns>
public static bool TryGetExceptionDetails(this ProblemDetails problemDetails, out SerializableException exception)
{
if (problemDetails.Extensions.TryGetValue(nameof(ProblemDetailsExtensionMembers.ExceptionDetails).ToCamelCase(), out var value))
return value.TryParseSerializableException(out exception);

exception = null;
return false;
}

/// <summary>
/// Try to parse a SerializableException from an object.
/// </summary>
/// <param name="value">The object to parse the SerializableException from.</param>
/// <param name="exception">When this method returns, the exception from the object, if the object can be parsed to an SerializableException; otherwise, null.</param>
/// <returns>true if the object can be parsed to an SerializableException; otherwise, false.</returns>
public static bool TryParseSerializableException(this object value, out SerializableException exception)
{
exception = null;

if (value is SerializableException serializableException)
exception = serializableException;

if (value is JsonElement jsonElement)
{
var json = jsonElement.GetRawText();
exception = json.ReadAsSerializableException();
}

return exception != null;
}

/// <summary>
/// Try to get the errors dictionary from the ProblemDetails.
/// </summary>
/// <param name="problemDetails">ProblemDetails to get the errors dictionary from.</param>
/// <param name="errors">When this method returns, the errors dictionary from the ProblemDetails, if the ProblemDetails contains errors dictionary; otherwise, null.</param>
/// <returns>true if the ProblemDetails contains errors dictionary; otherwise, false.</returns>
public static bool TryGetErrors(this ProblemDetails problemDetails, out IDictionary<string, object[]> errors)
{
if (problemDetails.Extensions.TryGetValue(nameof(ProblemDetailsExtensionMembers.Errors).ToCamelCase(), out var value))
return value.TryParseErrors(out errors);

errors = null;
return false;
}

/// <summary>
/// Try to parse a errors dictionary from an object.
/// </summary>
/// <param name="value">The object to parse the errors dictionary from.</param>
/// <param name="errors">When this method returns, the errors dictionary from the object, if the object can be parsed to an errors dictionary; otherwise, null.</param>
/// <returns>true if the object can be parsed to an errors dictionary; otherwise, false.</returns>
public static bool TryParseErrors(this object value, out IDictionary<string, object[]> errors)
{
errors = null;

#pragma warning disable RCS1220 // Use pattern matching instead of combination of 'is' operator and cast operator.
if (value is IDictionary<string, object[]>)
errors = (IDictionary<string, object[]>)value;
#pragma warning restore RCS1220 // Use pattern matching instead of combination of 'is' operator and cast operator.

return errors != null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Opw.HttpExceptions.AspNetCore
namespace Opw.HttpExceptions.AspNetCore.Serialization
{
// https://github.com/aspnet/AspNetCore/blob/a99ab25700e6c8a58ca75ee59621b8b671431706/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsJsonConverter.cs
internal class ProblemDetailsJsonConverter : JsonConverter<ProblemDetails>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Opw.HttpExceptions.AspNetCore.Serialization
{
internal class SerializableExceptionJsonConverter : JsonConverter<SerializableException>
{
private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type");
private static readonly JsonEncodedText HelpLink = JsonEncodedText.Encode("helpLink");
private static readonly JsonEncodedText HResult = JsonEncodedText.Encode("hResult");
private static readonly JsonEncodedText Message = JsonEncodedText.Encode("message");
private static readonly JsonEncodedText Source = JsonEncodedText.Encode("source");
private static readonly JsonEncodedText StackTrace = JsonEncodedText.Encode("stackTrace");
private static readonly JsonEncodedText Data = JsonEncodedText.Encode("data");
private static readonly JsonEncodedText InnerException = JsonEncodedText.Encode("innerException");

public override SerializableException Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var serializableException = new SerializableException();

if (!reader.Read())
{
throw new JsonException("UnexpectedJsonEnd");
}

while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
ReadValue(ref reader, serializableException, options);
}

if (reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException("UnexpectedJsonEnd");
}

return serializableException;
}

public override void Write(Utf8JsonWriter writer, SerializableException value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}

internal static void ReadValue(ref Utf8JsonReader reader, SerializableException value, JsonSerializerOptions options)
{
if (TryReadStringProperty(ref reader, Type, out var propertyValue))
{
value.Type = propertyValue;
}
else if (TryReadStringProperty(ref reader, HelpLink, out propertyValue))
{
value.HelpLink = propertyValue;
}
else if (TryReadStringProperty(ref reader, Message, out propertyValue))
{
value.Message = propertyValue;
}
else if (TryReadStringProperty(ref reader, Source, out propertyValue))
{
value.Source = propertyValue;
}
else if (TryReadStringProperty(ref reader, StackTrace, out propertyValue))
{
value.StackTrace = propertyValue;
}
else if (reader.TokenType == JsonTokenType.PropertyName && reader.ValueTextEquals(HResult.EncodedUtf8Bytes))
{
reader.Read();
if (reader.TokenType == JsonTokenType.Null)
{
// Nothing to do here.
}
else
{
value.HResult = reader.GetInt32();
}
}
else
{
var propertyName = reader.GetString();

string json;
using (var document = JsonDocument.ParseValue(ref reader))
{
json = document.RootElement.Clone().GetRawText();
}

if (propertyName == Data.ToString())
{
// TODO: Data property has not been implemented
}
else if (propertyName == InnerException.ToString())
{
try
{
value.InnerException = json.ReadAsSerializableException();
}
catch { }
}
}
}

internal static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value)
{
if (reader.TokenType != JsonTokenType.PropertyName || !reader.ValueTextEquals(propertyName.EncodedUtf8Bytes))
{
value = default;
return false;
}

reader.Read();
value = reader.GetString();
return true;
}
}
}
10 changes: 9 additions & 1 deletion src/Opw.HttpExceptions.AspNetCore/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using Opw.HttpExceptions.AspNetCore.Serialization;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace Opw.HttpExceptions.AspNetCore
{
//TODO: create a tools package for these extensions?
internal static class StringExtensions
{
internal static string ToCamelCase(this string s)
Expand Down Expand Up @@ -49,5 +50,12 @@ internal static string RemoveDiacritics(this string s)

return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
}

internal static SerializableException ReadAsSerializableException(this string json)
{
var converter = new SerializableExceptionJsonConverter();
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
return converter.Read(ref reader, typeof(SerializableException), new JsonSerializerOptions());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace Opw.HttpExceptions.AspNetCore
{
Expand All @@ -10,13 +10,14 @@ public static class HttpContentExtensions
public static async Task<T> ReadAsAsync<T>(this HttpContent content)
{
var str = await content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(str);
// WARNING: Newtonsoft can only be used here because this is a test project
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(str);
}

public static StringContent ToJsonContent(this object obj)
public static StringContent ToJsonContent(this object obj, string mediaType = "application/json")
{
var str = JsonConvert.SerializeObject(obj);
return new StringContent(str, Encoding.UTF8, "application/json");
var str = JsonSerializer.Serialize(obj, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
return new StringContent(str, Encoding.UTF8, mediaType);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using System;
using Xunit;
using System.Net;

namespace Opw.HttpExceptions.AspNetCore
{
public class HttpContentExtensionsTests
{
[Fact]
public void ReadAsProblemDetails_Should_ReturnProblemDetails()
{
ApplicationException applicationException;
try
{
throw new ApplicationException("Error!");
}
catch (ApplicationException ex)
{
applicationException = ex;
}

var mapper = TestHelper.CreateProblemDetailsExceptionMapper<Exception>(true);
var actionResult = mapper.Map(applicationException, new DefaultHttpContext());

actionResult.Should().BeOfType<ProblemDetailsResult>();
var problemDetailsResult = (ProblemDetailsResult)actionResult;
var jsonContent = problemDetailsResult.Value.ToJsonContent("application/problem+json");

var result = jsonContent.ReadAsProblemDetails();

result.ShouldNotBeNull(HttpStatusCode.InternalServerError);
result.Title.Should().Be("Application");
result.Detail.Should().Be("Error!");
result.Type.Should().Be("error:application");
result.Instance.Should().BeNull();

var exception = result.ShouldHaveExceptionDetails();

exception.Type.Should().Be("ApplicationException");
exception.Message.Should().Be("Error!");
}
}
}
Loading

0 comments on commit b126647

Please sign in to comment.