Add converters for ProblemDetails and ValidationProblemDetails (#12216)
* Add converters for ProblemDetails and ValidationProblemDetails ProblemDetails & ValidationProblemDetails require IgnoreNullValues to be applied on the types. We'll use a converter to workaround the absence of this feature Fixes https://github.com/aspnet/AspNetCore/issues/11522
This commit is contained in:
parent
f8c22859a0
commit
a99ab25700
|
|
@ -960,12 +960,17 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public partial class ProblemDetails
|
||||
{
|
||||
public ProblemDetails() { }
|
||||
[System.Text.Json.Serialization.JsonPropertyNameAttribute("detail")]
|
||||
public string Detail { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[System.Text.Json.Serialization.JsonExtensionDataAttribute]
|
||||
public System.Collections.Generic.IDictionary<string, object> Extensions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
[System.Text.Json.Serialization.JsonPropertyNameAttribute("instance")]
|
||||
public string Instance { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[System.Text.Json.Serialization.JsonPropertyNameAttribute("status")]
|
||||
public int? Status { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[System.Text.Json.Serialization.JsonPropertyNameAttribute("title")]
|
||||
public string Title { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[System.Text.Json.Serialization.JsonPropertyNameAttribute("type")]
|
||||
public string Type { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
}
|
||||
[System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
|
||||
|
|
@ -1249,6 +1254,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public ValidationProblemDetails() { }
|
||||
public ValidationProblemDetails(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) { }
|
||||
public ValidationProblemDetails(System.Collections.Generic.IDictionary<string, string[]> errors) { }
|
||||
[System.Text.Json.Serialization.JsonPropertyNameAttribute("errors")]
|
||||
public System.Collections.Generic.IDictionary<string, string[]> Errors { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
}
|
||||
public partial class VirtualFileResult : Microsoft.AspNetCore.Mvc.FileResult
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
internal class ProblemDetailsJsonConverter : JsonConverter<ProblemDetails>
|
||||
{
|
||||
private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type");
|
||||
private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title");
|
||||
private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status");
|
||||
private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail");
|
||||
private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance");
|
||||
|
||||
public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var problemDetails = new ProblemDetails();
|
||||
|
||||
if (!reader.Read())
|
||||
{
|
||||
throw new JsonException(Resources.UnexpectedJsonEnd);
|
||||
}
|
||||
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
|
||||
{
|
||||
ReadValue(ref reader, problemDetails, options);
|
||||
}
|
||||
|
||||
if (reader.TokenType != JsonTokenType.EndObject)
|
||||
{
|
||||
throw new JsonException(Resources.UnexpectedJsonEnd);
|
||||
}
|
||||
|
||||
return problemDetails;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
WriteProblemDetails(writer, value, options);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
internal static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options)
|
||||
{
|
||||
if (TryReadStringProperty(ref reader, Type, out var propertyValue))
|
||||
{
|
||||
value.Type = propertyValue;
|
||||
}
|
||||
else if (TryReadStringProperty(ref reader, Title, out propertyValue))
|
||||
{
|
||||
value.Title = propertyValue;
|
||||
}
|
||||
else if (TryReadStringProperty(ref reader, Detail, out propertyValue))
|
||||
{
|
||||
value.Detail = propertyValue;
|
||||
}
|
||||
else if (TryReadStringProperty(ref reader, Instance, out propertyValue))
|
||||
{
|
||||
value.Instance = propertyValue;
|
||||
}
|
||||
else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes))
|
||||
{
|
||||
reader.Read();
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
// Nothing to do here.
|
||||
}
|
||||
else
|
||||
{
|
||||
value.Status = reader.GetInt32();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var key = reader.GetString();
|
||||
reader.Read();
|
||||
value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof(object), options);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value)
|
||||
{
|
||||
if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
reader.Read();
|
||||
value = reader.GetString();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.Type != null)
|
||||
{
|
||||
writer.WriteString(Type, value.Type);
|
||||
}
|
||||
|
||||
if (value.Title != null)
|
||||
{
|
||||
writer.WriteString(Title, value.Title);
|
||||
}
|
||||
|
||||
if (value.Status != null)
|
||||
{
|
||||
writer.WriteNumber(Status, value.Status.Value);
|
||||
}
|
||||
|
||||
if (value.Detail != null)
|
||||
{
|
||||
writer.WriteString(Detail, value.Detail);
|
||||
}
|
||||
|
||||
if (value.Instance != null)
|
||||
{
|
||||
writer.WriteString(Instance, value.Instance);
|
||||
}
|
||||
|
||||
foreach (var kvp in value.Extensions)
|
||||
{
|
||||
writer.WritePropertyName(kvp.Key);
|
||||
JsonSerializer.Serialize(writer, kvp.Value, kvp.Value?.GetType() ?? typeof(object), options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using static Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsJsonConverter;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
internal class ValidationProblemDetailsJsonConverter : JsonConverter<ValidationProblemDetails>
|
||||
{
|
||||
private static readonly JsonEncodedText Errors = JsonEncodedText.Encode("errors");
|
||||
|
||||
public override ValidationProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var problemDetails = new ValidationProblemDetails();
|
||||
|
||||
if (!reader.Read())
|
||||
{
|
||||
throw new JsonException(Resources.UnexpectedJsonEnd);
|
||||
}
|
||||
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
|
||||
{
|
||||
if (reader.ValueTextEquals(Errors.EncodedUtf8Bytes))
|
||||
{
|
||||
var errors = JsonSerializer.Deserialize<Dictionary<string, string[]>>(ref reader, options);
|
||||
foreach (var item in errors)
|
||||
{
|
||||
problemDetails.Errors[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadValue(ref reader, problemDetails, options);
|
||||
}
|
||||
}
|
||||
|
||||
if (reader.TokenType != JsonTokenType.EndObject)
|
||||
{
|
||||
throw new JsonException(Resources.UnexpectedJsonEnd);
|
||||
}
|
||||
|
||||
return problemDetails;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ValidationProblemDetails value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
WriteProblemDetails(writer, value, options);
|
||||
|
||||
writer.WriteStartObject(Errors);
|
||||
foreach (var kvp in value.Errors)
|
||||
{
|
||||
writer.WritePropertyName(kvp.Key);
|
||||
JsonSerializer.Serialize(writer, kvp.Value, kvp.Value?.GetType() ?? typeof(object), options);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(ProblemDetailsJsonConverter))]
|
||||
public class ProblemDetails
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -18,6 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
|
||||
/// "about:blank".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -25,21 +28,25 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
|
||||
/// see[RFC7231], Section 3.4).
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public int? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable explanation specific to this occurrence of the problem.
|
||||
/// </summary>
|
||||
[JsonPropertyName("detail")]
|
||||
public string Detail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced.
|
||||
/// </summary>
|
||||
[JsonPropertyName("instance")]
|
||||
public string Instance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
|
|
@ -26,36 +26,36 @@
|
|||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
|
|
@ -507,4 +507,7 @@
|
|||
<data name="ObjectResultExecutor_MaxEnumerationExceeded" xml:space="preserve">
|
||||
<value>'{0}' reached the configured maximum size of the buffer when enumerating a value of type '{1}'. This limit is in place to prevent infinite streams of 'IAsyncEnumerable<>' from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting '{1}' into a list rather than increasing the limit.</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="UnexpectedJsonEnd" xml:space="preserve">
|
||||
<value>Unexcepted end when reading JSON.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
|
|
@ -11,6 +13,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <summary>
|
||||
/// A <see cref="ProblemDetails"/> for validation errors.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(ValidationProblemDetailsJsonConverter))]
|
||||
public class ValidationProblemDetails : ProblemDetails
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -83,6 +86,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <summary>
|
||||
/// Gets the validation errors associated with this instance of <see cref="ValidationProblemDetails"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("errors")]
|
||||
public IDictionary<string, string[]> Errors { get; } = new Dictionary<string, string[]>(StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class ProblemDetailsConverterTest
|
||||
{
|
||||
private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().JsonSerializerOptions;
|
||||
|
||||
[Fact]
|
||||
public void Read_ThrowsIfJsonIsIncomplete()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{";
|
||||
var converter = new ProblemDetailsJsonConverter();
|
||||
|
||||
// Act & Assert
|
||||
var ex = Record.Exception(() =>
|
||||
{
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
|
||||
converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions);
|
||||
});
|
||||
Assert.IsAssignableFrom<JsonException>(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_Works()
|
||||
{
|
||||
// Arrange
|
||||
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
|
||||
var title = "Not found";
|
||||
var status = 404;
|
||||
var detail = "Product not found";
|
||||
var instance = "http://example.com/products/14";
|
||||
var traceId = "|37dd3dd5-4a9619f953c40a16.";
|
||||
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"}}";
|
||||
var converter = new ProblemDetailsJsonConverter();
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
|
||||
|
||||
// Act
|
||||
var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions);
|
||||
|
||||
Assert.Equal(type, problemDetails.Type);
|
||||
Assert.Equal(title, problemDetails.Title);
|
||||
Assert.Equal(status, problemDetails.Status);
|
||||
Assert.Equal(instance, problemDetails.Instance);
|
||||
Assert.Equal(detail, problemDetails.Detail);
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("traceId", kvp.Key);
|
||||
Assert.Equal(traceId, kvp.Value.ToString());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_WithSomeMissingValues_Works()
|
||||
{
|
||||
// Arrange
|
||||
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
|
||||
var title = "Not found";
|
||||
var status = 404;
|
||||
var traceId = "|37dd3dd5-4a9619f953c40a16.";
|
||||
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"}}";
|
||||
var converter = new ProblemDetailsJsonConverter();
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
|
||||
|
||||
// Act
|
||||
var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions);
|
||||
|
||||
Assert.Equal(type, problemDetails.Type);
|
||||
Assert.Equal(title, problemDetails.Title);
|
||||
Assert.Equal(status, problemDetails.Status);
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("traceId", kvp.Key);
|
||||
Assert.Equal(traceId, kvp.Value.ToString());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_Works()
|
||||
{
|
||||
// Arrange
|
||||
var traceId = "|37dd3dd5-4a9619f953c40a16.";
|
||||
var value = new ProblemDetails
|
||||
{
|
||||
Title = "Not found",
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
Status = 404,
|
||||
Detail = "Product not found",
|
||||
Instance = "http://example.com/products/14",
|
||||
Extensions =
|
||||
{
|
||||
{ "traceId", traceId },
|
||||
{ "some-data", new[] { "value1", "value2" } }
|
||||
}
|
||||
};
|
||||
var expected = $"{{\"type\":\"{JsonEncodedText.Encode(value.Type)}\",\"title\":\"{value.Title}\",\"status\":{value.Status},\"detail\":\"{value.Detail}\",\"instance\":\"{JsonEncodedText.Encode(value.Instance)}\",\"traceId\":\"{traceId}\",\"some-data\":[\"value1\",\"value2\"]}}";
|
||||
var converter = new ProblemDetailsJsonConverter();
|
||||
var stream = new MemoryStream();
|
||||
|
||||
// Act
|
||||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
converter.Write(writer, value, JsonSerializerOptions);
|
||||
}
|
||||
|
||||
// Assert
|
||||
var actual = Encoding.UTF8.GetString(stream.ToArray());
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_WithSomeMissingContent_Works()
|
||||
{
|
||||
// Arrange
|
||||
var value = new ProblemDetails
|
||||
{
|
||||
Title = "Not found",
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
Status = 404,
|
||||
};
|
||||
var expected = $"{{\"type\":\"{JsonEncodedText.Encode(value.Type)}\",\"title\":\"{value.Title}\",\"status\":{value.Status}}}";
|
||||
var converter = new ProblemDetailsJsonConverter();
|
||||
var stream = new MemoryStream();
|
||||
|
||||
// Act
|
||||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
converter.Write(writer, value, JsonSerializerOptions);
|
||||
}
|
||||
|
||||
// Assert
|
||||
var actual = Encoding.UTF8.GetString(stream.ToArray());
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class ValidationProblemDetailsConverterTest
|
||||
{
|
||||
private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().JsonSerializerOptions;
|
||||
|
||||
[Fact]
|
||||
public void Read_Works()
|
||||
{
|
||||
// Arrange
|
||||
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
|
||||
var title = "Not found";
|
||||
var status = 404;
|
||||
var detail = "Product not found";
|
||||
var instance = "http://example.com/products/14";
|
||||
var traceId = "|37dd3dd5-4a9619f953c40a16.";
|
||||
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"," +
|
||||
"\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}";
|
||||
var converter = new ValidationProblemDetailsJsonConverter();
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
|
||||
|
||||
// Act
|
||||
var problemDetails = converter.Read(ref reader, typeof(ValidationProblemDetails), JsonSerializerOptions);
|
||||
|
||||
Assert.Equal(type, problemDetails.Type);
|
||||
Assert.Equal(title, problemDetails.Title);
|
||||
Assert.Equal(status, problemDetails.Status);
|
||||
Assert.Equal(instance, problemDetails.Instance);
|
||||
Assert.Equal(detail, problemDetails.Detail);
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("traceId", kvp.Key);
|
||||
Assert.Equal(traceId, kvp.Value.ToString());
|
||||
});
|
||||
Assert.Collection(
|
||||
problemDetails.Errors.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("key0", kvp.Key);
|
||||
Assert.Equal(new[] { "error0" }, kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("key1", kvp.Key);
|
||||
Assert.Equal(new[] { "error1", "error2" }, kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_WithSomeMissingValues_Works()
|
||||
{
|
||||
// Arrange
|
||||
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
|
||||
var title = "Not found";
|
||||
var status = 404;
|
||||
var traceId = "|37dd3dd5-4a9619f953c40a16.";
|
||||
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"}}";
|
||||
var converter = new ProblemDetailsJsonConverter();
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
|
||||
|
||||
// Act
|
||||
var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions);
|
||||
|
||||
Assert.Equal(type, problemDetails.Type);
|
||||
Assert.Equal(title, problemDetails.Title);
|
||||
Assert.Equal(status, problemDetails.Status);
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("traceId", kvp.Key);
|
||||
Assert.Equal(traceId, kvp.Value.ToString());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_Works()
|
||||
{
|
||||
// Arrange
|
||||
var traceId = "|37dd3dd5-4a9619f953c40a16.";
|
||||
var value = new ProblemDetails
|
||||
{
|
||||
Title = "Not found",
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
Status = 404,
|
||||
Detail = "Product not found",
|
||||
Instance = "http://example.com/products/14",
|
||||
Extensions =
|
||||
{
|
||||
{ "traceId", traceId },
|
||||
{ "some-data", new[] { "value1", "value2" } }
|
||||
}
|
||||
};
|
||||
var expected = $"{{\"type\":\"{JsonEncodedText.Encode(value.Type)}\",\"title\":\"{value.Title}\",\"status\":{value.Status},\"detail\":\"{value.Detail}\",\"instance\":\"{JsonEncodedText.Encode(value.Instance)}\",\"traceId\":\"{traceId}\",\"some-data\":[\"value1\",\"value2\"]}}";
|
||||
var converter = new ProblemDetailsJsonConverter();
|
||||
var stream = new MemoryStream();
|
||||
|
||||
// Act
|
||||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
converter.Write(writer, value, JsonSerializerOptions);
|
||||
}
|
||||
|
||||
// Assert
|
||||
var actual = Encoding.UTF8.GetString(stream.ToArray());
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_WithSomeMissingContent_Works()
|
||||
{
|
||||
// Arrange
|
||||
var value = new ProblemDetails
|
||||
{
|
||||
Title = "Not found",
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
Status = 404,
|
||||
};
|
||||
var expected = $"{{\"type\":\"{JsonEncodedText.Encode(value.Type)}\",\"title\":\"{value.Title}\",\"status\":{value.Status}}}";
|
||||
var converter = new ProblemDetailsJsonConverter();
|
||||
var stream = new MemoryStream();
|
||||
|
||||
// Act
|
||||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
converter.Write(writer, value, JsonSerializerOptions);
|
||||
}
|
||||
|
||||
// Assert
|
||||
var actual = Encoding.UTF8.GetString(stream.ToArray());
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -340,31 +340,31 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
{
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/pull/11460")]
|
||||
[Fact]
|
||||
public override Task ActionsReturnBadRequest_WhenModelStateIsInvalid()
|
||||
{
|
||||
return base.ActionsReturnBadRequest_WhenModelStateIsInvalid();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/38769")]
|
||||
[Fact]
|
||||
public override Task ClientErrorResultFilterExecutesForStatusCodeResults()
|
||||
{
|
||||
return base.ClientErrorResultFilterExecutesForStatusCodeResults();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/38769")]
|
||||
[Fact]
|
||||
public override Task SerializingProblemDetails_IgnoresNullValuedProperties()
|
||||
{
|
||||
return base.SerializingProblemDetails_IgnoresNullValuedProperties();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/38769")]
|
||||
[Fact]
|
||||
public override Task SerializingProblemDetails_WithAllValuesSpecified()
|
||||
{
|
||||
return base.SerializingProblemDetails_WithAllValuesSpecified();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/38769")]
|
||||
[Fact]
|
||||
public override Task SerializingValidationProblemDetails_WithExtensionData()
|
||||
{
|
||||
return base.SerializingValidationProblemDetails_WithExtensionData();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using FormatterWebSite.Controllers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
|
|
@ -33,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
[Fact]
|
||||
public override Task Formatting_DictionaryType() => base.Formatting_DictionaryType();
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/11522")]
|
||||
[Fact]
|
||||
public override Task Formatting_ProblemDetails() => base.Formatting_ProblemDetails();
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Reference in New Issue