Use ClientErrorData to configure ClientErrorResultFilter

Fixes #8289
This commit is contained in:
Pranav K 2018-08-23 10:37:58 -07:00
parent 17d72c2b94
commit 667ad4daff
16 changed files with 265 additions and 124 deletions

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc
/// </summary> /// </summary>
public class ApiBehaviorOptions : IEnumerable<ICompatibilitySwitch> public class ApiBehaviorOptions : IEnumerable<ICompatibilitySwitch>
{ {
private readonly CompatibilitySwitch<bool> _suppressUseClientErrorFactory; private readonly CompatibilitySwitch<bool> _suppressMapClientErrors;
private readonly CompatibilitySwitch<bool> _suppressUseValidationProblemDetailsForInvalidModelStateResponses; private readonly CompatibilitySwitch<bool> _suppressUseValidationProblemDetailsForInvalidModelStateResponses;
private readonly ICompatibilitySwitch[] _switches; private readonly ICompatibilitySwitch[] _switches;
@ -26,11 +26,11 @@ namespace Microsoft.AspNetCore.Mvc
/// </summary> /// </summary>
public ApiBehaviorOptions() public ApiBehaviorOptions()
{ {
_suppressUseClientErrorFactory = new CompatibilitySwitch<bool>(nameof(SuppressUseClientErrorFactory)); _suppressMapClientErrors = new CompatibilitySwitch<bool>(nameof(SuppressMapClientErrors));
_suppressUseValidationProblemDetailsForInvalidModelStateResponses = new CompatibilitySwitch<bool>(nameof(SuppressUseValidationProblemDetailsForInvalidModelStateResponses)); _suppressUseValidationProblemDetailsForInvalidModelStateResponses = new CompatibilitySwitch<bool>(nameof(SuppressUseValidationProblemDetailsForInvalidModelStateResponses));
_switches = new[] _switches = new[]
{ {
_suppressUseClientErrorFactory, _suppressMapClientErrors,
_suppressUseValidationProblemDetailsForInvalidModelStateResponses, _suppressUseValidationProblemDetailsForInvalidModelStateResponses,
}; };
} }
@ -71,12 +71,16 @@ namespace Microsoft.AspNetCore.Mvc
public bool SuppressConsumesConstraintForFormFileParameters { get; set; } public bool SuppressConsumesConstraintForFormFileParameters { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value that determines if controllers with <see cref="ApiControllerAttribute"/> use <see cref="ClientErrorFactory"/> /// Gets or sets a value that determines if controllers with <see cref="ApiControllerAttribute"/>
/// to transform certain certain client errors. /// transform certain certain client errors.
/// <para> /// <para>
/// When <c>false</c>, <see cref="ClientErrorFactory"/> is used to transform <see cref="IClientErrorActionResult"/> to the value /// When <c>false</c>, a result filter is added to API controller actions that transforms <see cref="IClientErrorActionResult"/>.
/// specified by the factory. In the default case, this converts <see cref="StatusCodeResult"/> instances to an <see cref="ObjectResult"/> /// By default, <see cref="ClientErrorMapping"/> is used to map <see cref="IClientErrorActionResult"/> to a
/// with <see cref="ProblemDetails"/>. /// <see cref="ProblemDetails"/> instance (returned as the value for <see cref="ObjectResult"/>).
/// </para>
/// <para>
/// To customize the output of the filter (for e.g. to return a different error type), register a custom
/// implementation of of <see cref="IClientErrorFactory"/> in the service collection.
/// </para> /// </para>
/// </summary> /// </summary>
/// <value> /// <value>
@ -102,11 +106,11 @@ namespace Microsoft.AspNetCore.Mvc
/// higher then this setting will have the value <see langword="true"/> unless explicitly configured. /// higher then this setting will have the value <see langword="true"/> unless explicitly configured.
/// </para> /// </para>
/// </remarks> /// </remarks>
public bool SuppressUseClientErrorFactory public bool SuppressMapClientErrors
{ {
// Note: When compatibility switches are removed in 3.0, this property should be retained as a regular boolean property. // Note: When compatibility switches are removed in 3.0, this property should be retained as a regular boolean property.
get => _suppressUseClientErrorFactory.Value; get => _suppressMapClientErrors.Value;
set => _suppressUseClientErrorFactory.Value = value; set => _suppressMapClientErrors.Value = value;
} }
/// <summary> /// <summary>
@ -148,11 +152,15 @@ namespace Microsoft.AspNetCore.Mvc
} }
/// <summary> /// <summary>
/// Gets a map of HTTP status codes to <see cref="IActionResult"/> factories. /// Gets a map of HTTP status codes to <see cref="ClientErrorData"/>. Configured values
/// Configured factories are used when <see cref="SuppressUseClientErrorFactory"/> is <see langword="false"/>. /// are used to transform <see cref="IClientErrorActionResult"/> to an <see cref="ObjectResult"/>
/// instance where the <see cref="ObjectResult.Value"/> is <see cref="ProblemDetails"/>.
/// <para>
/// Use of this feature can be disabled by resetting <see cref="SuppressMapClientErrors"/>.
/// </para>
/// </summary> /// </summary>
public IDictionary<int, Func<ActionContext, IActionResult>> ClientErrorFactory { get; } = public IDictionary<int, ClientErrorData> ClientErrorMapping { get; } =
new Dictionary<int, Func<ActionContext, IActionResult>>(); new Dictionary<int, ClientErrorData>();
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator() IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator()
{ {

View File

@ -0,0 +1,29 @@
// 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
{
/// <summary>
/// Information for producing client errors. This type is used to configure client errors
/// produced by consumers of <see cref="ApiBehaviorOptions.ClientErrorMapping"/>.
/// </summary>
public class ClientErrorData
{
/// <summary>
/// Gets or sets a link (URI) that describes the client error.
/// </summary>
/// <remarks>
/// By default, this maps to <see cref="ProblemDetails.Type"/>.
/// </remarks>
public string Link { get; set; }
/// <summary>
/// Gets or sets the summary of the client error.
/// </summary>
/// <remarks>
/// By default, this maps to <see cref="ProblemDetails.Title"/> and should not change
/// between multiple occurences of the same error.
/// </remarks>
public string Title { get; set; }
}
}

View File

@ -257,6 +257,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>(); services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>(); services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>(); services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
// //
// Route Handlers // Route Handlers

View File

@ -2,7 +2,6 @@
// 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; using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -11,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{ {
internal class ClientErrorResultFilter : IAlwaysRunResultFilter, IOrderedFilter internal class ClientErrorResultFilter : IAlwaysRunResultFilter, IOrderedFilter
{ {
private readonly IDictionary<int, Func<ActionContext, IActionResult>> _clientErrorFactory; private readonly IClientErrorFactory _clientErrorFactory;
private readonly ILogger<ClientErrorResultFilter> _logger; private readonly ILogger<ClientErrorResultFilter> _logger;
/// <summary> /// <summary>
@ -20,10 +19,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
public int Order => -2000; public int Order => -2000;
public ClientErrorResultFilter( public ClientErrorResultFilter(
ApiBehaviorOptions apiBehaviorOptions, IClientErrorFactory clientErrorFactory,
ILogger<ClientErrorResultFilter> logger) ILogger<ClientErrorResultFilter> logger)
{ {
_clientErrorFactory = apiBehaviorOptions?.ClientErrorFactory ?? throw new ArgumentNullException(nameof(apiBehaviorOptions)); _clientErrorFactory = clientErrorFactory ?? throw new ArgumentNullException(nameof(clientErrorFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@ -38,16 +37,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Result is IClientErrorActionResult clientErrorActionResult && if (!(context.Result is IClientErrorActionResult clientError))
clientErrorActionResult.StatusCode is int statusCode &&
_clientErrorFactory.TryGetValue(statusCode, out var factory))
{ {
var result = factory(context); return;
_logger.TransformingClientError(context.Result.GetType(), result?.GetType(), statusCode);
context.Result = factory(context);
} }
var result = _clientErrorFactory.GetClientError(context, clientError);
if (result == null)
{
return;
}
_logger.TransformingClientError(context.Result.GetType(), result?.GetType(), clientError.StatusCode);
context.Result = result;
} }
} }
} }

View File

@ -0,0 +1,20 @@
// 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
{
/// <summary>
/// A factory for producing client errors. This contract is used by controllers annotated
/// with <see cref="ApiControllerAttribute"/> to transform <see cref="IClientErrorActionResult"/>.
/// </summary>
public interface IClientErrorFactory
{
/// <summary>
/// Transforms <paramref name="clientError"/> for the specified <paramref name="actionContext"/>.
/// </summary>
/// <param name="actionContext">The <see cref="ActionContext"/>.</param>
/// <param name="clientError">The <see cref="IClientErrorActionResult"/>.</param>
/// <returns>THe <see cref="IActionResult"/> that would be returned to the client.</returns>
IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError);
}
}

View File

@ -0,0 +1,44 @@
// 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.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
internal class ProblemDetailsClientErrorFactory : IClientErrorFactory
{
private readonly ApiBehaviorOptions _options;
public ProblemDetailsClientErrorFactory(IOptions<ApiBehaviorOptions> options)
{
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
}
public IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError)
{
var problemDetails = new ProblemDetails
{
Status = clientError.StatusCode,
Type = "about:blank",
};
if (clientError.StatusCode is int statusCode &&
_options.ClientErrorMapping.TryGetValue(statusCode, out var errorData))
{
problemDetails.Title = errorData.Title;
problemDetails.Type = errorData.Link;
}
return new ObjectResult(problemDetails)
{
StatusCode = problemDetails.Status,
ContentTypes =
{
"application/problem+json",
"application/problem+xml",
},
};
}
}
}

View File

@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public ApiBehaviorApplicationModelProvider( public ApiBehaviorApplicationModelProvider(
IOptions<ApiBehaviorOptions> apiBehaviorOptions, IOptions<ApiBehaviorOptions> apiBehaviorOptions,
IModelMetadataProvider modelMetadataProvider, IModelMetadataProvider modelMetadataProvider,
IClientErrorFactory clientErrorFactory,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
{ {
_apiBehaviorOptions = apiBehaviorOptions.Value; _apiBehaviorOptions = apiBehaviorOptions.Value;
@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
loggerFactory.CreateLogger<ModelStateInvalidFilter>()); loggerFactory.CreateLogger<ModelStateInvalidFilter>());
_clientErrorResultFilter = new ClientErrorResultFilter( _clientErrorResultFilter = new ClientErrorResultFilter(
_apiBehaviorOptions, clientErrorFactory,
loggerFactory.CreateLogger<ClientErrorResultFilter>()); loggerFactory.CreateLogger<ClientErrorResultFilter>());
} }
@ -158,7 +159,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private void AddClientErrorFilter(ActionModel actionModel) private void AddClientErrorFilter(ActionModel actionModel)
{ {
if (_apiBehaviorOptions.SuppressUseClientErrorFactory) if (_apiBehaviorOptions.SuppressMapClientErrors)
{ {
return; return;
} }

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (Version < CompatibilityVersion.Version_2_2) if (Version < CompatibilityVersion.Version_2_2)
{ {
dictionary[nameof(ApiBehaviorOptions.SuppressUseClientErrorFactory)] = true; dictionary[nameof(ApiBehaviorOptions.SuppressMapClientErrors)] = true;
dictionary[nameof(ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses)] = true; dictionary[nameof(ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses)] = true;
} }
@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
options.InvalidModelStateResponseFactory = DefaultFactory; options.InvalidModelStateResponseFactory = DefaultFactory;
ConfigureClientErrorFactories(options); ConfigureClientErrorMapping(options);
} }
public override void PostConfigure(string name, ApiBehaviorOptions options) public override void PostConfigure(string name, ApiBehaviorOptions options)
@ -57,9 +57,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
base.PostConfigure(name, options); base.PostConfigure(name, options);
// We want to use problem details factory only if // We want to use problem details factory only if
// (a) it has not been opted out of (SuppressUseClientErrorFactory = true) // (a) it has not been opted out of (SuppressMapClientErrors = true)
// (b) a different factory was configured // (b) a different factory was configured
if (!options.SuppressUseClientErrorFactory && if (!options.SuppressMapClientErrors &&
object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory)) object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory))
{ {
options.InvalidModelStateResponseFactory = ProblemDetailsFactory; options.InvalidModelStateResponseFactory = ProblemDetailsFactory;
@ -67,77 +67,55 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
// Internal for unit testing // Internal for unit testing
internal static void ConfigureClientErrorFactories(ApiBehaviorOptions options) internal static void ConfigureClientErrorMapping(ApiBehaviorOptions options)
{ {
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[400] = new ClientErrorData
{ {
Status = 400, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = Resources.ApiConventions_Title_400, Title = Resources.ApiConventions_Title_400,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[401] = new ClientErrorData
{ {
Status = 401, Link = "https://tools.ietf.org/html/rfc7235#section-3.1",
Type = "https://tools.ietf.org/html/rfc7235#section-3.1",
Title = Resources.ApiConventions_Title_401, Title = Resources.ApiConventions_Title_401,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[403] = new ClientErrorData
{ {
Status = 403, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.3",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3",
Title = Resources.ApiConventions_Title_403, Title = Resources.ApiConventions_Title_403,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[404] = new ClientErrorData
{ {
Status = 404, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = Resources.ApiConventions_Title_404, Title = Resources.ApiConventions_Title_404,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[406] = new ClientErrorData
{ {
Status = 406, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.6",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.6",
Title = Resources.ApiConventions_Title_406, Title = Resources.ApiConventions_Title_406,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[409] = new ClientErrorData
{ {
Status = 409, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
Title = Resources.ApiConventions_Title_409, Title = Resources.ApiConventions_Title_409,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[415] = new ClientErrorData
{ {
Status = 415, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.13",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.13",
Title = Resources.ApiConventions_Title_415, Title = Resources.ApiConventions_Title_415,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[422] = new ClientErrorData
{ {
Status = 422, Link = "https://tools.ietf.org/html/rfc4918#section-11.2",
Type = "https://tools.ietf.org/html/rfc4918#section-11.2",
Title = Resources.ApiConventions_Title_422, Title = Resources.ApiConventions_Title_422,
}); };
void AddClientErrorFactory(ProblemDetails problemDetails)
{
var statusCode = problemDetails.Status.Value;
options.ClientErrorFactory[statusCode] = _ => new ObjectResult(problemDetails)
{
StatusCode = statusCode,
ContentTypes =
{
"application/problem+json",
"application/problem+xml",
},
};
}
} }
private static IActionResult DefaultInvalidModelStateResponse(ActionContext context) private static IActionResult DefaultInvalidModelStateResponse(ActionContext context)

View File

@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static readonly Action<ILogger, Type, Type, Type, Exception> _notMostEffectiveFilter; private static readonly Action<ILogger, Type, Type, Type, Exception> _notMostEffectiveFilter;
private static readonly Action<ILogger, IEnumerable<IOutputFormatter>, Exception> _registeredOutputFormatters; private static readonly Action<ILogger, IEnumerable<IOutputFormatter>, Exception> _registeredOutputFormatters;
private static readonly Action<ILogger, Type, Type, int, Exception> _transformingClientError; private static readonly Action<ILogger, Type, int?, Type, Exception> _transformingClientError;
static MvcCoreLoggerExtensions() static MvcCoreLoggerExtensions()
{ {
@ -651,10 +651,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
48, 48,
"Skipped binding parameter '{ParameterName}' since its binding information disallowed it for the current request."); "Skipped binding parameter '{ParameterName}' since its binding information disallowed it for the current request.");
_transformingClientError = LoggerMessage.Define<Type, Type, int>( _transformingClientError = LoggerMessage.Define<Type, int?, Type>(
LogLevel.Trace, LogLevel.Trace,
new EventId(49, nameof(Infrastructure.ClientErrorResultFilter)), new EventId(49, nameof(Infrastructure.ClientErrorResultFilter)),
"Replacing {InitialActionResultType} with status code {StatusCode} with {ReplacedActionResultType} produced from ClientErrorFactory'."); "Replacing {InitialActionResultType} with status code {StatusCode} with {ReplacedActionResultType}.");
} }
public static void RegisteredOutputFormatters(this ILogger logger, IEnumerable<IOutputFormatter> outputFormatters) public static void RegisteredOutputFormatters(this ILogger logger, IEnumerable<IOutputFormatter> outputFormatters)
@ -1585,9 +1585,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
} }
public static void TransformingClientError(this ILogger logger, Type initialType, Type replacedType, int statusCode) public static void TransformingClientError(this ILogger logger, Type initialType, Type replacedType, int? statusCode)
{ {
_transformingClientError(logger, initialType, replacedType, statusCode, null); _transformingClientError(logger, initialType, statusCode, replacedType, null);
} }
private static void LogFilterExecutionPlan( private static void LogFilterExecutionPlan(

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
public string Type { get; set; } public string Type { get; set; }
/// <summary> /// <summary>
/// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence /// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence
/// 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>
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc
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>
public string Instance { get; set; } public string Instance { get; set; }
} }

View File

@ -32,35 +32,25 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
} }
[Fact] [Fact]
public void OnResultExecuting_DoesNothing_IfStatusCodeDoesNotExistInApiBehaviorOptions() public void OnResultExecuting_DoesNothing_IfTransformedValueIsNull()
{ {
// Arrange // Arrange
var actionResult = new NotFoundResult(); var actionResult = new NotFoundResult();
var context = GetContext(actionResult); var context = GetContext(actionResult);
var filter = GetFilter(new ApiBehaviorOptions()); var factory = new Mock<IClientErrorFactory>();
factory
// Act .Setup(f => f.GetClientError(It.IsAny<ActionContext>(), It.IsAny<IClientErrorActionResult>()))
filter.OnResultExecuting(context); .Returns((IActionResult)null)
.Verifiable();
// Assert
Assert.Same(actionResult, context.Result); var filter = new ClientErrorResultFilter(factory.Object, NullLogger<ClientErrorResultFilter>.Instance);
}
[Fact]
public void OnResultExecuting_DoesNothing_IfResultDoesNotHaveStatusCode()
{
// Arrange
var actionResult = new Mock<IActionResult>()
.As<IClientErrorActionResult>()
.Object;
var context = GetContext(actionResult);
var filter = GetFilter(new ApiBehaviorOptions());
// Act // Act
filter.OnResultExecuting(context); filter.OnResultExecuting(context);
// Assert // Assert
Assert.Same(actionResult, context.Result); Assert.Same(actionResult, context.Result);
factory.Verify();
} }
[Fact] [Fact]
@ -78,18 +68,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
Assert.Same(Result, context.Result); Assert.Same(Result, context.Result);
} }
private static ClientErrorResultFilter GetFilter(ApiBehaviorOptions options = null) private static ClientErrorResultFilter GetFilter()
{ {
var apiBehaviorOptions = options ?? GetOptions(); var factory = Mock.Of<IClientErrorFactory>(
var filter = new ClientErrorResultFilter(apiBehaviorOptions, NullLogger<ClientErrorResultFilter>.Instance); f => f.GetClientError(It.IsAny<ActionContext>(), It.IsAny<IClientErrorActionResult>()) == Result);
return filter;
}
private static ApiBehaviorOptions GetOptions() return new ClientErrorResultFilter(factory, NullLogger<ClientErrorResultFilter>.Instance);
{
var apiBehaviorOptions = new ApiBehaviorOptions();
apiBehaviorOptions.ClientErrorFactory[404] = _ => Result;
return apiBehaviorOptions;
} }
private static ResultExecutingContext GetContext(IActionResult actionResult) private static ResultExecutingContext GetContext(IActionResult actionResult)

View File

@ -0,0 +1,65 @@
// 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.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
public class ProblemDetalsClientErrorFactoryTest
{
[Fact]
public void GetClientError_ReturnsProblemDetails_IfNoMappingWasFound()
{
// Arrange
var clientError = new UnsupportedMediaTypeResult();
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
{
ClientErrorMapping =
{
[405] = new ClientErrorData { Link = "Some link", Title = "Summary" },
},
}));
// Act
var result = factory.GetClientError(new ActionContext(), clientError);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes);
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
Assert.Equal(415, problemDetails.Status);
Assert.Equal("about:blank", problemDetails.Type);
Assert.Null(problemDetails.Title);
Assert.Null(problemDetails.Detail);
Assert.Null(problemDetails.Instance);
}
[Fact]
public void GetClientError_ReturnsProblemDetails()
{
// Arrange
var clientError = new UnsupportedMediaTypeResult();
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
{
ClientErrorMapping =
{
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
},
}));
// Act
var result = factory.GetClientError(new ActionContext(), clientError);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes);
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
Assert.Equal(415, problemDetails.Status);
Assert.Equal("Some link", problemDetails.Type);
Assert.Equal("Summary", problemDetails.Title);
Assert.Null(problemDetails.Detail);
Assert.Null(problemDetails.Instance);
}
}
}

View File

@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal namespace Microsoft.AspNetCore.Mvc.Internal
@ -1081,7 +1082,7 @@ Environment.NewLine + "int b";
var context = GetContext(typeof(TestApiController)); var context = GetContext(typeof(TestApiController));
var options = new ApiBehaviorOptions var options = new ApiBehaviorOptions
{ {
SuppressUseClientErrorFactory = true, SuppressMapClientErrors = true,
InvalidModelStateResponseFactory = _ => null, InvalidModelStateResponseFactory = _ => null,
}; };
var provider = GetProvider(options); var provider = GetProvider(options);
@ -1122,7 +1123,11 @@ Environment.NewLine + "int b";
var loggerFactory = NullLoggerFactory.Instance; var loggerFactory = NullLoggerFactory.Instance;
modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider(); modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
return new ApiBehaviorApplicationModelProvider(optionsAccessor, modelMetadataProvider, loggerFactory); return new ApiBehaviorApplicationModelProvider(
optionsAccessor,
modelMetadataProvider,
Mock.Of<IClientErrorFactory>(),
loggerFactory);
} }
private static ApplicationModelProviderContext GetContext( private static ApplicationModelProviderContext GetContext(

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
[Fact] [Fact]
public void Configure_AddsClientErrorFactories() public void Configure_AddsClientErrorMappings()
{ {
// Arrange // Arrange
var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, }; var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, };
@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
optionsSetup.Configure(options); optionsSetup.Configure(options);
// Assert // Assert
Assert.Equal(expected, options.ClientErrorFactory.Keys); Assert.Equal(expected, options.ClientErrorMapping.Keys);
} }
[Fact] [Fact]

View File

@ -97,6 +97,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert // Assert
await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType); await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType);
var content = await response.Content.ReadAsStringAsync();
var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(content);
Assert.Equal((int)HttpStatusCode.UnsupportedMediaType, problemDetails.Status);
Assert.Equal("Unsupported Media Type", problemDetails.Title);
} }
[Fact] [Fact]

View File

@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.False(mvcOptions.EnableEndpointRouting); Assert.False(mvcOptions.EnableEndpointRouting);
Assert.Null(mvcOptions.MaxValidationDepth); Assert.Null(mvcOptions.MaxValidationDepth);
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory); Assert.True(apiBehaviorOptions.SuppressMapClientErrors);
} }
[Fact] [Fact]
@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.False(mvcOptions.EnableEndpointRouting); Assert.False(mvcOptions.EnableEndpointRouting);
Assert.Null(mvcOptions.MaxValidationDepth); Assert.Null(mvcOptions.MaxValidationDepth);
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory); Assert.True(apiBehaviorOptions.SuppressMapClientErrors);
} }
[Fact] [Fact]
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.True(mvcOptions.EnableEndpointRouting); Assert.True(mvcOptions.EnableEndpointRouting);
Assert.Equal(32, mvcOptions.MaxValidationDepth); Assert.Equal(32, mvcOptions.MaxValidationDepth);
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.False(apiBehaviorOptions.SuppressUseClientErrorFactory); Assert.False(apiBehaviorOptions.SuppressMapClientErrors);
} }
[Fact] [Fact]
@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.True(mvcOptions.EnableEndpointRouting); Assert.True(mvcOptions.EnableEndpointRouting);
Assert.Equal(32, mvcOptions.MaxValidationDepth); Assert.Equal(32, mvcOptions.MaxValidationDepth);
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.False(apiBehaviorOptions.SuppressUseClientErrorFactory); Assert.False(apiBehaviorOptions.SuppressMapClientErrors);
} }
// This just does the minimum needed to be able to resolve these options. // This just does the minimum needed to be able to resolve these options.