Introduce opinionated API defaults.
* Introduce ProblemDescriptionAttribute to enhance some 4xx messages and produce better API description. * Introduce IErrorDescriptionProvider to modify the shape of error response. Fixes #6785, Fixes #6786
This commit is contained in:
parent
037c1ec47d
commit
776c2604f8
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
|
|
@ -9,6 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an API exposed by this application.
|
/// Represents an API exposed by this application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[DebuggerDisplay("{ActionDescriptor.DisplayName,nq}")]
|
||||||
public class ApiDescription
|
public class ApiDescription
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddSingleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
|
services.TryAddSingleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
|
||||||
services.TryAddEnumerable(
|
services.TryAddEnumerable(
|
||||||
ServiceDescriptor.Transient<IApiDescriptionProvider, DefaultApiDescriptionProvider>());
|
ServiceDescriptor.Transient<IApiDescriptionProvider, DefaultApiDescriptionProvider>());
|
||||||
|
services.TryAddEnumerable(
|
||||||
|
ServiceDescriptor.Transient<IApiDescriptionProvider, ProblemDetailsApiDescriptionProvider>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
|
{
|
||||||
|
public class ProblemDetailsApiDescriptionProvider : IApiDescriptionProvider
|
||||||
|
{
|
||||||
|
private readonly IModelMetadataProvider _modelMetadaProvider;
|
||||||
|
|
||||||
|
public ProblemDetailsApiDescriptionProvider(IModelMetadataProvider modelMetadataProvider)
|
||||||
|
{
|
||||||
|
_modelMetadaProvider = modelMetadataProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// The order is set to execute after the <see cref="DefaultApiDescriptionProvider"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public int Order => -1000 + 10;
|
||||||
|
|
||||||
|
public void OnProvidersExecuted(ApiDescriptionProviderContext context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnProvidersExecuting(ApiDescriptionProviderContext context)
|
||||||
|
{
|
||||||
|
foreach (var apiDescription in context.Results)
|
||||||
|
{
|
||||||
|
if (!apiDescription.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is ProblemDetailsAttribute))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters = apiDescription.ActionDescriptor.Parameters.Concat(apiDescription.ActionDescriptor.BoundProperties);
|
||||||
|
if (parameters.Any())
|
||||||
|
{
|
||||||
|
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(StatusCodes.Status400BadRequest));
|
||||||
|
|
||||||
|
if (parameters.Any(p => p.Name.EndsWith("id", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(StatusCodes.Status404NotFound));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have a good way to signal a "default" response type. We'll use 0 to indicate this until we come up
|
||||||
|
// with something better.
|
||||||
|
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(statusCode: 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApiResponseType CreateProblemResponse(int statusCode)
|
||||||
|
{
|
||||||
|
return new ApiResponseType
|
||||||
|
{
|
||||||
|
ApiResponseFormats = new List<ApiResponseFormat>
|
||||||
|
{
|
||||||
|
new ApiResponseFormat
|
||||||
|
{
|
||||||
|
MediaType = "application/problem+json",
|
||||||
|
},
|
||||||
|
new ApiResponseFormat
|
||||||
|
{
|
||||||
|
MediaType = "application/problem+xml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ModelMetadata = _modelMetadaProvider.GetMetadataForType(typeof(ProblemDetails)),
|
||||||
|
StatusCode = statusCode,
|
||||||
|
Type = typeof(ProblemDetails),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1503,7 +1503,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||||
[NonAction]
|
[NonAction]
|
||||||
public virtual BadRequestObjectResult ValidationProblem(ValidationProblemDescription descriptor)
|
public virtual ActionResult ValidationProblem(ValidationProblemDetails descriptor)
|
||||||
{
|
{
|
||||||
if (descriptor == null)
|
if (descriptor == null)
|
||||||
{
|
{
|
||||||
|
|
@ -1518,14 +1518,14 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||||
[NonAction]
|
[NonAction]
|
||||||
public virtual BadRequestObjectResult ValidationProblem(ModelStateDictionary modelStateDictionary)
|
public virtual ActionResult ValidationProblem(ModelStateDictionary modelStateDictionary)
|
||||||
{
|
{
|
||||||
if (modelStateDictionary == null)
|
if (modelStateDictionary == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(modelStateDictionary));
|
throw new ArgumentNullException(nameof(modelStateDictionary));
|
||||||
}
|
}
|
||||||
|
|
||||||
var validationProblem = new ValidationProblemDescription(modelStateDictionary);
|
var validationProblem = new ValidationProblemDetails(modelStateDictionary);
|
||||||
return new BadRequestObjectResult(validationProblem);
|
return new BadRequestObjectResult(validationProblem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1535,9 +1535,9 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||||
[NonAction]
|
[NonAction]
|
||||||
public virtual BadRequestObjectResult ValidationProblem()
|
public virtual ActionResult ValidationProblem()
|
||||||
{
|
{
|
||||||
var validationProblem = new ValidationProblemDescription(ModelState);
|
var validationProblem = new ValidationProblemDetails(ModelState);
|
||||||
return new BadRequestObjectResult(validationProblem);
|
return new BadRequestObjectResult(validationProblem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddTransient<RequestSizeLimitFilter>();
|
services.TryAddTransient<RequestSizeLimitFilter>();
|
||||||
services.TryAddTransient<DisableRequestSizeLimitFilter>();
|
services.TryAddTransient<DisableRequestSizeLimitFilter>();
|
||||||
|
|
||||||
|
// Error description
|
||||||
|
services.TryAddSingleton<IErrorDescriptionFactory, DefaultErrorDescriptorFactory>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// ModelBinding, Validation
|
// ModelBinding, Validation
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
{
|
||||||
|
public class ErrorDescriptionContext
|
||||||
|
{
|
||||||
|
public ErrorDescriptionContext(ActionDescriptor actionDescriptor)
|
||||||
|
{
|
||||||
|
ActionDescriptor = actionDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActionDescriptor ActionDescriptor { get; }
|
||||||
|
|
||||||
|
public object Result { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines a contract for creating or modifying an error response.
|
||||||
|
/// </summary>
|
||||||
|
public interface IErrorDescriptionFactory
|
||||||
|
{
|
||||||
|
object CreateErrorDescription(ActionDescriptor actionDescriptor, object result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
{
|
||||||
|
public interface IErrorDescriptorProvider
|
||||||
|
{
|
||||||
|
int Order { get; }
|
||||||
|
|
||||||
|
void OnProvidersExecuting(ErrorDescriptionContext context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
public class DefaultErrorDescriptorFactory : IErrorDescriptionFactory
|
||||||
|
{
|
||||||
|
private readonly IErrorDescriptorProvider[] _providers;
|
||||||
|
|
||||||
|
public DefaultErrorDescriptorFactory(IEnumerable<IErrorDescriptorProvider> providers)
|
||||||
|
{
|
||||||
|
if (providers == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(providers));
|
||||||
|
}
|
||||||
|
|
||||||
|
_providers = providers.OrderBy(p => p.Order).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object CreateErrorDescription(ActionDescriptor actionDescriptor, object result)
|
||||||
|
{
|
||||||
|
if (actionDescriptor == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(actionDescriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new ErrorDescriptionContext(actionDescriptor)
|
||||||
|
{
|
||||||
|
Result = result,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 0; i < _providers.Length; i++)
|
||||||
|
{
|
||||||
|
_providers[i].OnProvidersExecuting(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.Result ?? result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ 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>
|
||||||
public class ProblemDescription
|
public class ProblemDetails
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when
|
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an <see cref="IFilterMetadata"/> that indicates to the framework that the current action conforms to well-known API behavior.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class ProblemDetailsAttribute : Attribute, IFilterMetadata
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
-->
|
-->
|
||||||
|
|
|
||||||
|
|
@ -9,19 +9,19 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A <see cref="ProblemDescription"/> for validation errors.
|
/// A <see cref="ProblemDetails"/> for validation errors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ValidationProblemDescription : ProblemDescription
|
public class ValidationProblemDetails : ProblemDetails
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Intializes a new instance of <see cref="ValidationProblemDescription"/>.
|
/// Intializes a new instance of <see cref="ValidationProblemDetails"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ValidationProblemDescription()
|
public ValidationProblemDetails()
|
||||||
{
|
{
|
||||||
Title = Resources.ValidationProblemDescription_Title;
|
Title = Resources.ValidationProblemDescription_Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidationProblemDescription(ModelStateDictionary modelState)
|
public ValidationProblemDetails(ModelStateDictionary modelState)
|
||||||
: this()
|
: this()
|
||||||
{
|
{
|
||||||
if (modelState == null)
|
if (modelState == null)
|
||||||
|
|
@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the validation errors associated with this instance of <see cref="ValidationProblemDescription"/>.
|
/// Gets or sets the validation errors associated with this instance of <see cref="ValidationProblemDetails"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDictionary<string, string[]> Errors { get; } = new Dictionary<string, string[]>(StringComparer.Ordinal);
|
public IDictionary<string, string[]> Errors { get; } = new Dictionary<string, string[]>(StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
@ -6,13 +6,13 @@ using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
{
|
{
|
||||||
public class ValidationProblemDescriptionTest
|
public class ValidationProblemDetailsTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_SetsTitle()
|
public void Constructor_SetsTitle()
|
||||||
{
|
{
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
var problemDescription = new ValidationProblemDescription();
|
var problemDescription = new ValidationProblemDetails();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("One or more validation errors occured.", problemDescription.Title);
|
Assert.Equal("One or more validation errors occured.", problemDescription.Title);
|
||||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
modelStateDictionary.AddModelError("key3", "error3");
|
modelStateDictionary.AddModelError("key3", "error3");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var problemDescription = new ValidationProblemDescription(modelStateDictionary);
|
var problemDescription = new ValidationProblemDetails(modelStateDictionary);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("One or more validation errors occured.", problemDescription.Title);
|
Assert.Equal("One or more validation errors occured.", problemDescription.Title);
|
||||||
|
|
@ -1065,6 +1065,88 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("ApiExplorerInboundOutbound/SuppressedForLinkGeneration", description.RelativePath);
|
Assert.Equal("ApiExplorerInboundOutbound/SuppressedForLinkGeneration", description.RelativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProblemDetails_AddsProblemAsDefaultErrorResult()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var body = await Client.GetStringAsync("ApiExplorerProblemDetails/ActionWithoutParameters");
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Collection(
|
||||||
|
description.SupportedResponseTypes,
|
||||||
|
response =>
|
||||||
|
{
|
||||||
|
Assert.Equal(0, response.StatusCode);
|
||||||
|
AssertProblemDetails(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProblemDetails_AddsProblemAsErrorResultForBadResult_WhenActionHasParameters()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var body = await Client.GetStringAsync("ApiExplorerProblemDetails/ActionWithSomeParameters");
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Collection(
|
||||||
|
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
|
||||||
|
response =>
|
||||||
|
{
|
||||||
|
Assert.Equal(0, response.StatusCode);
|
||||||
|
AssertProblemDetails(response);
|
||||||
|
},
|
||||||
|
response => Assert.Equal(200, response.StatusCode),
|
||||||
|
response =>
|
||||||
|
{
|
||||||
|
Assert.Equal(400, response.StatusCode);
|
||||||
|
AssertProblemDetails(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("ApiExplorerProblemDetails/ActionWithIdParameter")]
|
||||||
|
[InlineData("ApiExplorerProblemDetails/ActionWithIdSuffixParameter")]
|
||||||
|
public async Task ProblemDetails_AddsProblemAsErrorResultForNotFoundResult_WhenActionHasAnIdParameters(string url)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var body = await Client.GetStringAsync(url);
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Collection(
|
||||||
|
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
|
||||||
|
response =>
|
||||||
|
{
|
||||||
|
Assert.Equal(0, response.StatusCode);
|
||||||
|
AssertProblemDetails(response);
|
||||||
|
},
|
||||||
|
response => Assert.Equal(200, response.StatusCode),
|
||||||
|
response =>
|
||||||
|
{
|
||||||
|
Assert.Equal(400, response.StatusCode);
|
||||||
|
AssertProblemDetails(response);
|
||||||
|
},
|
||||||
|
response =>
|
||||||
|
{
|
||||||
|
Assert.Equal(404, response.StatusCode);
|
||||||
|
AssertProblemDetails(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AssertProblemDetails(ApiExplorerResponseType response)
|
||||||
|
{
|
||||||
|
Assert.Equal("Microsoft.AspNetCore.Mvc.ProblemDetails", response.ResponseType);
|
||||||
|
Assert.Collection(
|
||||||
|
GetSortedMediaTypes(response),
|
||||||
|
mediaType => Assert.Equal("application/problem+json", mediaType),
|
||||||
|
mediaType => Assert.Equal("application/problem+xml", mediaType));
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
|
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
|
||||||
{
|
{
|
||||||
return apiResponseType.ResponseFormats
|
return apiResponseType.ResponseFormats
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
|
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
|
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.TagHelpers;
|
using Microsoft.AspNetCore.Mvc.TagHelpers;
|
||||||
|
|
@ -418,6 +417,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
new Type[]
|
new Type[]
|
||||||
{
|
{
|
||||||
typeof(DefaultApiDescriptionProvider),
|
typeof(DefaultApiDescriptionProvider),
|
||||||
|
typeof(ProblemDetailsApiDescriptionProvider),
|
||||||
typeof(JsonPatchOperationsArrayProvider),
|
typeof(JsonPatchOperationsArrayProvider),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ApiExplorerWebSite
|
||||||
|
{
|
||||||
|
[Route("ApiExplorerProblemDetails/[action]")]
|
||||||
|
[ProblemDetails]
|
||||||
|
public class ApiExplorerProblemDetailsController : Controller
|
||||||
|
{
|
||||||
|
public IActionResult ActionWithoutParameters() => Ok();
|
||||||
|
|
||||||
|
public void ActionWithSomeParameters(object input)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ActionWithIdParameter(int id, string name)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ActionWithIdSuffixParameter(int personId, string personName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue