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 partial class ProblemDetails
|
||||||
{
|
{
|
||||||
public ProblemDetails() { }
|
public ProblemDetails() { }
|
||||||
|
[System.Text.Json.Serialization.JsonPropertyNameAttribute("detail")]
|
||||||
public string Detail { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public string Detail { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
[System.Text.Json.Serialization.JsonExtensionDataAttribute]
|
[System.Text.Json.Serialization.JsonExtensionDataAttribute]
|
||||||
public System.Collections.Generic.IDictionary<string, object> Extensions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
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 { } }
|
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 { } }
|
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 { } }
|
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 { } }
|
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)]
|
[System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
|
||||||
|
|
@ -1249,6 +1254,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
public ValidationProblemDetails() { }
|
public ValidationProblemDetails() { }
|
||||||
public ValidationProblemDetails(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) { }
|
public ValidationProblemDetails(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) { }
|
||||||
public ValidationProblemDetails(System.Collections.Generic.IDictionary<string, string[]> errors) { }
|
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 System.Collections.Generic.IDictionary<string, string[]> Errors { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
}
|
}
|
||||||
public partial class VirtualFileResult : Microsoft.AspNetCore.Mvc.FileResult
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
|
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(ProblemDetailsJsonConverter))]
|
||||||
public class ProblemDetails
|
public class ProblemDetails
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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
|
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
|
||||||
/// "about:blank".
|
/// "about:blank".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -25,21 +28,25 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
|
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
|
||||||
/// see[RFC7231], Section 3.4).
|
/// see[RFC7231], Section 3.4).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonPropertyName("title")]
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
|
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonPropertyName("status")]
|
||||||
public int? Status { get; set; }
|
public int? Status { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A human-readable explanation specific to this occurrence of the problem.
|
/// A human-readable explanation specific to this occurrence of the problem.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonPropertyName("detail")]
|
||||||
public string Detail { get; set; }
|
public string Detail { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced.
|
/// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonPropertyName("instance")]
|
||||||
public string Instance { get; set; }
|
public string Instance { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<root>
|
<root>
|
||||||
<!--
|
<!--
|
||||||
Microsoft ResX Schema
|
Microsoft ResX Schema
|
||||||
|
|
||||||
Version 2.0
|
Version 2.0
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
The primary goals of this format is to allow a simple XML format
|
||||||
that is mostly human readable. The generation and parsing of the
|
that is mostly human readable. The generation and parsing of the
|
||||||
various data types are done through the TypeConverter classes
|
various data types are done through the TypeConverter classes
|
||||||
associated with the data types.
|
associated with the data types.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
... ado.net/XML headers & schema ...
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
<resheader name="version">2.0</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>
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
<comment>This is a comment</comment>
|
<comment>This is a comment</comment>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
There are any number of "resheader" rows that contain simple
|
||||||
name/value pairs.
|
name/value pairs.
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
Each data row contains a name, and value. The row also contains a
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
text/value conversion through the TypeConverter architecture.
|
text/value conversion through the TypeConverter architecture.
|
||||||
Classes that don't support this are serialized and stored with the
|
Classes that don't support this are serialized and stored with the
|
||||||
mimetype set.
|
mimetype set.
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
The mimetype is used for serialized objects, and tells the
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
read any of the formats listed below.
|
read any of the formats listed below.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
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
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
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
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
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
|
: using a System.ComponentModel.TypeConverter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
-->
|
-->
|
||||||
|
|
@ -507,4 +507,7 @@
|
||||||
<data name="ObjectResultExecutor_MaxEnumerationExceeded" xml:space="preserve">
|
<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>
|
<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>
|
</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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
@ -11,6 +13,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A <see cref="ProblemDetails"/> for validation errors.
|
/// A <see cref="ProblemDetails"/> for validation errors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(ValidationProblemDetailsJsonConverter))]
|
||||||
public class ValidationProblemDetails : ProblemDetails
|
public class ValidationProblemDetails : ProblemDetails
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -83,6 +86,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the validation errors associated with this instance of <see cref="ValidationProblemDetails"/>.
|
/// Gets the validation errors associated with this instance of <see cref="ValidationProblemDetails"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonPropertyName("errors")]
|
||||||
public IDictionary<string, string[]> Errors { get; } = new Dictionary<string, string[]>(StringComparer.Ordinal);
|
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()
|
public override Task ActionsReturnBadRequest_WhenModelStateIsInvalid()
|
||||||
{
|
{
|
||||||
return base.ActionsReturnBadRequest_WhenModelStateIsInvalid();
|
return base.ActionsReturnBadRequest_WhenModelStateIsInvalid();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/38769")]
|
[Fact]
|
||||||
public override Task ClientErrorResultFilterExecutesForStatusCodeResults()
|
public override Task ClientErrorResultFilterExecutesForStatusCodeResults()
|
||||||
{
|
{
|
||||||
return base.ClientErrorResultFilterExecutesForStatusCodeResults();
|
return base.ClientErrorResultFilterExecutesForStatusCodeResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/38769")]
|
[Fact]
|
||||||
public override Task SerializingProblemDetails_IgnoresNullValuedProperties()
|
public override Task SerializingProblemDetails_IgnoresNullValuedProperties()
|
||||||
{
|
{
|
||||||
return base.SerializingProblemDetails_IgnoresNullValuedProperties();
|
return base.SerializingProblemDetails_IgnoresNullValuedProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/38769")]
|
[Fact]
|
||||||
public override Task SerializingProblemDetails_WithAllValuesSpecified()
|
public override Task SerializingProblemDetails_WithAllValuesSpecified()
|
||||||
{
|
{
|
||||||
return base.SerializingProblemDetails_WithAllValuesSpecified();
|
return base.SerializingProblemDetails_WithAllValuesSpecified();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/38769")]
|
[Fact]
|
||||||
public override Task SerializingValidationProblemDetails_WithExtensionData()
|
public override Task SerializingValidationProblemDetails_WithExtensionData()
|
||||||
{
|
{
|
||||||
return base.SerializingValidationProblemDetails_WithExtensionData();
|
return base.SerializingValidationProblemDetails_WithExtensionData();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FormatterWebSite.Controllers;
|
using FormatterWebSite.Controllers;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
@ -33,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public override Task Formatting_DictionaryType() => base.Formatting_DictionaryType();
|
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();
|
public override Task Formatting_ProblemDetails() => base.Formatting_ProblemDetails();
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue