diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApiBehaviorOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApiBehaviorOptions.cs index 54614a2465..b2b2c2cd6f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApiBehaviorOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApiBehaviorOptions.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc /// public class ApiBehaviorOptions : IEnumerable { - private readonly CompatibilitySwitch _suppressUseClientErrorFactory; + private readonly CompatibilitySwitch _suppressMapClientErrors; private readonly CompatibilitySwitch _suppressUseValidationProblemDetailsForInvalidModelStateResponses; private readonly ICompatibilitySwitch[] _switches; @@ -26,11 +26,11 @@ namespace Microsoft.AspNetCore.Mvc /// public ApiBehaviorOptions() { - _suppressUseClientErrorFactory = new CompatibilitySwitch(nameof(SuppressUseClientErrorFactory)); + _suppressMapClientErrors = new CompatibilitySwitch(nameof(SuppressMapClientErrors)); _suppressUseValidationProblemDetailsForInvalidModelStateResponses = new CompatibilitySwitch(nameof(SuppressUseValidationProblemDetailsForInvalidModelStateResponses)); _switches = new[] { - _suppressUseClientErrorFactory, + _suppressMapClientErrors, _suppressUseValidationProblemDetailsForInvalidModelStateResponses, }; } @@ -71,12 +71,16 @@ namespace Microsoft.AspNetCore.Mvc public bool SuppressConsumesConstraintForFormFileParameters { get; set; } /// - /// Gets or sets a value that determines if controllers with use - /// to transform certain certain client errors. + /// Gets or sets a value that determines if controllers with + /// transform certain certain client errors. /// - /// When false, is used to transform to the value - /// specified by the factory. In the default case, this converts instances to an - /// with . + /// When false, a result filter is added to API controller actions that transforms . + /// By default, is used to map to a + /// instance (returned as the value for ). + /// + /// + /// To customize the output of the filter (for e.g. to return a different error type), register a custom + /// implementation of of in the service collection. /// /// /// @@ -102,11 +106,11 @@ namespace Microsoft.AspNetCore.Mvc /// higher then this setting will have the value unless explicitly configured. /// /// - 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. - get => _suppressUseClientErrorFactory.Value; - set => _suppressUseClientErrorFactory.Value = value; + get => _suppressMapClientErrors.Value; + set => _suppressMapClientErrors.Value = value; } /// @@ -148,11 +152,15 @@ namespace Microsoft.AspNetCore.Mvc } /// - /// Gets a map of HTTP status codes to factories. - /// Configured factories are used when is . + /// Gets a map of HTTP status codes to . Configured values + /// are used to transform to an + /// instance where the is . + /// + /// Use of this feature can be disabled by resetting . + /// /// - public IDictionary> ClientErrorFactory { get; } = - new Dictionary>(); + public IDictionary ClientErrorMapping { get; } = + new Dictionary(); IEnumerator IEnumerable.GetEnumerator() { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ClientErrorData.cs b/src/Microsoft.AspNetCore.Mvc.Core/ClientErrorData.cs new file mode 100644 index 0000000000..38b3448ece --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ClientErrorData.cs @@ -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 +{ + /// + /// Information for producing client errors. This type is used to configure client errors + /// produced by consumers of . + /// + public class ClientErrorData + { + /// + /// Gets or sets a link (URI) that describes the client error. + /// + /// + /// By default, this maps to . + /// + public string Link { get; set; } + + /// + /// Gets or sets the summary of the client error. + /// + /// + /// By default, this maps to and should not change + /// between multiple occurences of the same error. + /// + public string Title { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 767aa0c33f..a0c082fdf6 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -257,6 +257,7 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton, RedirectToRouteResultExecutor>(); services.TryAddSingleton, RedirectToPageResultExecutor>(); services.TryAddSingleton, ContentResultExecutor>(); + services.TryAddSingleton(); // // Route Handlers diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilter.cs index a6482dfe09..a212c50ea4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilter.cs @@ -2,7 +2,6 @@ // 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 Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.Extensions.Logging; @@ -11,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure { internal class ClientErrorResultFilter : IAlwaysRunResultFilter, IOrderedFilter { - private readonly IDictionary> _clientErrorFactory; + private readonly IClientErrorFactory _clientErrorFactory; private readonly ILogger _logger; /// @@ -20,10 +19,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure public int Order => -2000; public ClientErrorResultFilter( - ApiBehaviorOptions apiBehaviorOptions, + IClientErrorFactory clientErrorFactory, ILogger logger) { - _clientErrorFactory = apiBehaviorOptions?.ClientErrorFactory ?? throw new ArgumentNullException(nameof(apiBehaviorOptions)); + _clientErrorFactory = clientErrorFactory ?? throw new ArgumentNullException(nameof(clientErrorFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -38,16 +37,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure throw new ArgumentNullException(nameof(context)); } - if (context.Result is IClientErrorActionResult clientErrorActionResult && - clientErrorActionResult.StatusCode is int statusCode && - _clientErrorFactory.TryGetValue(statusCode, out var factory)) + if (!(context.Result is IClientErrorActionResult clientError)) { - var result = factory(context); - - _logger.TransformingClientError(context.Result.GetType(), result?.GetType(), statusCode); - - context.Result = factory(context); + return; } + + var result = _clientErrorFactory.GetClientError(context, clientError); + if (result == null) + { + return; + } + + _logger.TransformingClientError(context.Result.GetType(), result?.GetType(), clientError.StatusCode); + context.Result = result; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IClientErrorFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IClientErrorFactory.cs new file mode 100644 index 0000000000..b592c52a9b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IClientErrorFactory.cs @@ -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 +{ + /// + /// A factory for producing client errors. This contract is used by controllers annotated + /// with to transform . + /// + public interface IClientErrorFactory + { + /// + /// Transforms for the specified . + /// + /// The . + /// The . + /// THe that would be returned to the client. + IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ProblemDetailsClientErrorFactory.cs new file mode 100644 index 0000000000..1bf1e3ac43 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -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 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", + }, + }; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs index bc1ac18c79..5b21f55378 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs @@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal public ApiBehaviorApplicationModelProvider( IOptions apiBehaviorOptions, IModelMetadataProvider modelMetadataProvider, + IClientErrorFactory clientErrorFactory, ILoggerFactory loggerFactory) { _apiBehaviorOptions = apiBehaviorOptions.Value; @@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal loggerFactory.CreateLogger()); _clientErrorResultFilter = new ClientErrorResultFilter( - _apiBehaviorOptions, + clientErrorFactory, loggerFactory.CreateLogger()); } @@ -158,7 +159,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal private void AddClientErrorFilter(ActionModel actionModel) { - if (_apiBehaviorOptions.SuppressUseClientErrorFactory) + if (_apiBehaviorOptions.SuppressMapClientErrors) { return; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorOptionsSetup.cs index 15c4ef8892..4d64d9bb84 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorOptionsSetup.cs @@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal if (Version < CompatibilityVersion.Version_2_2) { - dictionary[nameof(ApiBehaviorOptions.SuppressUseClientErrorFactory)] = true; + dictionary[nameof(ApiBehaviorOptions.SuppressMapClientErrors)] = true; dictionary[nameof(ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses)] = true; } @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal } options.InvalidModelStateResponseFactory = DefaultFactory; - ConfigureClientErrorFactories(options); + ConfigureClientErrorMapping(options); } public override void PostConfigure(string name, ApiBehaviorOptions options) @@ -57,9 +57,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal base.PostConfigure(name, options); // 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 - if (!options.SuppressUseClientErrorFactory && + if (!options.SuppressMapClientErrors && object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory)) { options.InvalidModelStateResponseFactory = ProblemDetailsFactory; @@ -67,77 +67,55 @@ namespace Microsoft.AspNetCore.Mvc.Internal } // 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, - Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1", + Link = "https://tools.ietf.org/html/rfc7231#section-6.5.1", Title = Resources.ApiConventions_Title_400, - }); + }; - AddClientErrorFactory(new ProblemDetails + options.ClientErrorMapping[401] = new ClientErrorData { - Status = 401, - Type = "https://tools.ietf.org/html/rfc7235#section-3.1", + Link = "https://tools.ietf.org/html/rfc7235#section-3.1", Title = Resources.ApiConventions_Title_401, - }); + }; - AddClientErrorFactory(new ProblemDetails + options.ClientErrorMapping[403] = new ClientErrorData { - Status = 403, - Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3", + Link = "https://tools.ietf.org/html/rfc7231#section-6.5.3", Title = Resources.ApiConventions_Title_403, - }); + }; - AddClientErrorFactory(new ProblemDetails + options.ClientErrorMapping[404] = new ClientErrorData { - Status = 404, - Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4", + Link = "https://tools.ietf.org/html/rfc7231#section-6.5.4", Title = Resources.ApiConventions_Title_404, - }); + }; - AddClientErrorFactory(new ProblemDetails + options.ClientErrorMapping[406] = new ClientErrorData { - Status = 406, - Type = "https://tools.ietf.org/html/rfc7231#section-6.5.6", + Link = "https://tools.ietf.org/html/rfc7231#section-6.5.6", Title = Resources.ApiConventions_Title_406, - }); + }; - AddClientErrorFactory(new ProblemDetails + options.ClientErrorMapping[409] = new ClientErrorData { - Status = 409, - Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8", + Link = "https://tools.ietf.org/html/rfc7231#section-6.5.8", Title = Resources.ApiConventions_Title_409, - }); + }; - AddClientErrorFactory(new ProblemDetails + options.ClientErrorMapping[415] = new ClientErrorData { - Status = 415, - Type = "https://tools.ietf.org/html/rfc7231#section-6.5.13", + Link = "https://tools.ietf.org/html/rfc7231#section-6.5.13", Title = Resources.ApiConventions_Title_415, - }); + }; - AddClientErrorFactory(new ProblemDetails + options.ClientErrorMapping[422] = new ClientErrorData { - Status = 422, - Type = "https://tools.ietf.org/html/rfc4918#section-11.2", + Link = "https://tools.ietf.org/html/rfc4918#section-11.2", 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) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs index 4c2d80972e..b4a223b8b2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs @@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal private static readonly Action _notMostEffectiveFilter; private static readonly Action, Exception> _registeredOutputFormatters; - private static readonly Action _transformingClientError; + private static readonly Action _transformingClientError; static MvcCoreLoggerExtensions() { @@ -651,10 +651,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal 48, "Skipped binding parameter '{ParameterName}' since its binding information disallowed it for the current request."); - _transformingClientError = LoggerMessage.Define( + _transformingClientError = LoggerMessage.Define( LogLevel.Trace, 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 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( diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ProblemDetails.cs b/src/Microsoft.AspNetCore.Mvc.Core/ProblemDetails.cs index 35ac215562..573419b0bc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ProblemDetails.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ProblemDetails.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc public string Type { get; set; } /// - /// 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; /// see[RFC7231], Section 3.4). /// @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc public string Detail { get; set; } /// - /// 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. /// public string Instance { get; set; } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ClientErrorResultFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ClientErrorResultFilterTest.cs index 7c3f55858d..83098744ff 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ClientErrorResultFilterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ClientErrorResultFilterTest.cs @@ -32,35 +32,25 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure } [Fact] - public void OnResultExecuting_DoesNothing_IfStatusCodeDoesNotExistInApiBehaviorOptions() + public void OnResultExecuting_DoesNothing_IfTransformedValueIsNull() { // Arrange var actionResult = new NotFoundResult(); var context = GetContext(actionResult); - var filter = GetFilter(new ApiBehaviorOptions()); - - // Act - filter.OnResultExecuting(context); - - // Assert - Assert.Same(actionResult, context.Result); - } - - [Fact] - public void OnResultExecuting_DoesNothing_IfResultDoesNotHaveStatusCode() - { - // Arrange - var actionResult = new Mock() - .As() - .Object; - var context = GetContext(actionResult); - var filter = GetFilter(new ApiBehaviorOptions()); + var factory = new Mock(); + factory + .Setup(f => f.GetClientError(It.IsAny(), It.IsAny())) + .Returns((IActionResult)null) + .Verifiable(); + + var filter = new ClientErrorResultFilter(factory.Object, NullLogger.Instance); // Act filter.OnResultExecuting(context); // Assert Assert.Same(actionResult, context.Result); + factory.Verify(); } [Fact] @@ -78,18 +68,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure Assert.Same(Result, context.Result); } - private static ClientErrorResultFilter GetFilter(ApiBehaviorOptions options = null) + private static ClientErrorResultFilter GetFilter() { - var apiBehaviorOptions = options ?? GetOptions(); - var filter = new ClientErrorResultFilter(apiBehaviorOptions, NullLogger.Instance); - return filter; - } + var factory = Mock.Of( + f => f.GetClientError(It.IsAny(), It.IsAny()) == Result); - private static ApiBehaviorOptions GetOptions() - { - var apiBehaviorOptions = new ApiBehaviorOptions(); - apiBehaviorOptions.ClientErrorFactory[404] = _ => Result; - return apiBehaviorOptions; + return new ClientErrorResultFilter(factory, NullLogger.Instance); } private static ResultExecutingContext GetContext(IActionResult actionResult) diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs new file mode 100644 index 0000000000..65a53a8158 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs @@ -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(result); + Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes); + var problemDetails = Assert.IsType(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(result); + Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes); + var problemDetails = Assert.IsType(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); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs index 3b5d4415de..c0c415573f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.Internal @@ -1081,7 +1082,7 @@ Environment.NewLine + "int b"; var context = GetContext(typeof(TestApiController)); var options = new ApiBehaviorOptions { - SuppressUseClientErrorFactory = true, + SuppressMapClientErrors = true, InvalidModelStateResponseFactory = _ => null, }; var provider = GetProvider(options); @@ -1122,7 +1123,11 @@ Environment.NewLine + "int b"; var loggerFactory = NullLoggerFactory.Instance; modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider(); - return new ApiBehaviorApplicationModelProvider(optionsAccessor, modelMetadataProvider, loggerFactory); + return new ApiBehaviorApplicationModelProvider( + optionsAccessor, + modelMetadataProvider, + Mock.Of(), + loggerFactory); } private static ApplicationModelProviderContext GetContext( diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorOptionsSetupTest.cs index 66f4f7933b..622a36e5ff 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorOptionsSetupTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorOptionsSetupTest.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal } [Fact] - public void Configure_AddsClientErrorFactories() + public void Configure_AddsClientErrorMappings() { // Arrange var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, }; @@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal optionsSetup.Configure(options); // Assert - Assert.Equal(expected, options.ClientErrorFactory.Keys); + Assert.Equal(expected, options.ClientErrorMapping.Keys); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiBehaviorTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiBehaviorTest.cs index 0d0a85999f..d3ec4d9e29 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiBehaviorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiBehaviorTest.cs @@ -97,6 +97,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType); + var content = await response.Content.ReadAsStringAsync(); + var problemDetails = JsonConvert.DeserializeObject(content); + Assert.Equal((int)HttpStatusCode.UnsupportedMediaType, problemDetails.Status); + Assert.Equal("Unsupported Media Type", problemDetails.Title); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs index 813c148bfe..2413eb34bf 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs @@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.False(mvcOptions.EnableEndpointRouting); Assert.Null(mvcOptions.MaxValidationDepth); Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); - Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory); + Assert.True(apiBehaviorOptions.SuppressMapClientErrors); } [Fact] @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.False(mvcOptions.EnableEndpointRouting); Assert.Null(mvcOptions.MaxValidationDepth); Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); - Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory); + Assert.True(apiBehaviorOptions.SuppressMapClientErrors); } [Fact] @@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.True(mvcOptions.EnableEndpointRouting); Assert.Equal(32, mvcOptions.MaxValidationDepth); Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); - Assert.False(apiBehaviorOptions.SuppressUseClientErrorFactory); + Assert.False(apiBehaviorOptions.SuppressMapClientErrors); } [Fact] @@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.True(mvcOptions.EnableEndpointRouting); Assert.Equal(32, mvcOptions.MaxValidationDepth); 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.