-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add serialization helper methods to the Opw.HttpExceptions.AspNetCore…
- Loading branch information
1 parent
f83ffcf
commit b126647
Showing
14 changed files
with
481 additions
and
91 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
src/Opw.HttpExceptions.AspNetCore/HttpContentExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
src/Opw.HttpExceptions.AspNetCore/ProblemDetailsExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
src/Opw.HttpExceptions.AspNetCore/Serialization/SerializableExceptionJsonConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
tests/Opw.HttpExceptions.AspNetCore.Tests/HttpContentExtensionsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!"); | ||
} | ||
} | ||
} |
Oops, something went wrong.