From 709b390157912de0d41b99ec562ed1456d33b6a4 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 5 Aug 2019 11:28:28 -0700 Subject: [PATCH] Add helper methods on ControllerBase to return ProblemDetails (#12298) * Add helper methods on ControllerBase to return ProblemDetails * Introduce ControllerBase.Problem and ValidationProblem overload that accepts optional parameters * Consistently use ClientErrorData when generating ProblemDetails * Clean-up InvalidModelStateResponseFactory initialization. Fixes https://github.com/aspnet/AspNetCore/issues/8537 --- ...osoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs | 11 + src/Mvc/Mvc.Core/src/ControllerBase.cs | 111 +++++++++-- .../ApiBehaviorOptionsSetup.cs | 66 +++--- .../MvcCoreServiceCollectionExtensions.cs | 3 +- .../DefaultProblemDetailsFactory.cs | 97 +++++++++ .../ProblemDetailsClientErrorFactory.cs | 30 +-- .../Infrastructure/ProblemDetailsFactory.cs | 52 +++++ src/Mvc/Mvc.Core/src/Resources.resx | 3 + src/Mvc/Mvc.Core/test/ControllerBaseTest.cs | 161 ++++++++++++++- .../ApiBehaviorOptionsSetupTest.cs | 120 ++++++----- .../MvcCoreServiceCollectionExtensionsTest.cs | 7 - .../Formatters/TranscodingReadStreamTest.cs | 2 +- .../ProblemDetailsFactoryTest.cs | 188 ++++++++++++++++++ .../ProblemDetalsClientErrorFactoryTest.cs | 17 +- .../Mvc.FunctionalTests/ApiBehaviorTest.cs | 4 + ...artupWithCustomInvalidModelStateFactory.cs | 10 +- 16 files changed, 716 insertions(+), 166 deletions(-) create mode 100644 src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs create mode 100644 src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsFactory.cs create mode 100644 src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs diff --git a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs index f44b184654..07646edce5 100644 --- a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs +++ b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs @@ -271,6 +271,7 @@ namespace Microsoft.AspNetCore.Mvc public Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinderFactory ModelBinderFactory { get { throw null; } set { } } public Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary ModelState { get { throw null; } } public Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IObjectModelValidator ObjectValidator { get { throw null; } set { } } + public Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory ProblemDetailsFactory { get { throw null; } set { } } public Microsoft.AspNetCore.Http.HttpRequest Request { get { throw null; } } public Microsoft.AspNetCore.Http.HttpResponse Response { get { throw null; } } public Microsoft.AspNetCore.Routing.RouteData RouteData { get { throw null; } } @@ -445,6 +446,8 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.PhysicalFileResult PhysicalFile(string physicalPath, string contentType, string fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue entityTag, bool enableRangeProcessing) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] + public virtual Microsoft.AspNetCore.Mvc.ObjectResult Problem(string detail = null, string instance = null, int? statusCode = default(int?), string title = null, string type = null) { throw null; } + [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.RedirectResult Redirect(string url) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.RedirectResult RedirectPermanent(string url) { throw null; } @@ -586,6 +589,8 @@ namespace Microsoft.AspNetCore.Mvc public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ValidationProblemDetails descriptor) { throw null; } + [Microsoft.AspNetCore.Mvc.NonActionAttribute] + public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem(string detail = null, string instance = null, int? statusCode = default(int?), string title = null, string type = null, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary = null) { throw null; } } public partial class ControllerContext : Microsoft.AspNetCore.Mvc.ActionContext { @@ -2439,6 +2444,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure public long Length { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } } + public abstract partial class ProblemDetailsFactory + { + protected ProblemDetailsFactory() { } + public abstract Microsoft.AspNetCore.Mvc.ProblemDetails CreateProblemDetails(Microsoft.AspNetCore.Http.HttpContext httpContext, int? statusCode = default(int?), string title = null, string type = null, string detail = null, string instance = null); + public abstract Microsoft.AspNetCore.Mvc.ValidationProblemDetails CreateValidationProblemDetails(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary, int? statusCode = default(int?), string title = null, string type = null, string detail = null, string instance = null); + } public partial class RedirectResultExecutor : Microsoft.AspNetCore.Mvc.Infrastructure.IActionResultExecutor { public RedirectResultExecutor(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory urlHelperFactory) { } diff --git a/src/Mvc/Mvc.Core/src/ControllerBase.cs b/src/Mvc/Mvc.Core/src/ControllerBase.cs index 1bca7a09f3..e502ca144a 100644 --- a/src/Mvc/Mvc.Core/src/ControllerBase.cs +++ b/src/Mvc/Mvc.Core/src/ControllerBase.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc @@ -31,6 +32,7 @@ namespace Microsoft.AspNetCore.Mvc private IModelBinderFactory _modelBinderFactory; private IObjectModelValidator _objectValidator; private IUrlHelper _url; + private ProblemDetailsFactory _problemDetailsFactory; /// /// Gets the for the executing action. @@ -189,6 +191,28 @@ namespace Microsoft.AspNetCore.Mvc } } + public ProblemDetailsFactory ProblemDetailsFactory + { + get + { + if (_problemDetailsFactory == null) + { + _problemDetailsFactory = HttpContext?.RequestServices?.GetRequiredService(); + } + + return _problemDetailsFactory; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _problemDetailsFactory = value; + } + } + /// /// Gets the for user associated with the executing action. /// @@ -1821,6 +1845,34 @@ namespace Microsoft.AspNetCore.Mvc public virtual ConflictObjectResult Conflict([ActionResultObjectValue] ModelStateDictionary modelState) => new ConflictObjectResult(modelState); + /// + /// Creates an that produces a response. + /// + /// The value for .. + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The created for the response. + [NonAction] + public virtual ObjectResult Problem( + string detail = null, + string instance = null, + int? statusCode = null, + string title = null, + string type = null) + { + var problemDetails = ProblemDetailsFactory.CreateProblemDetails( + HttpContext, + statusCode: statusCode ?? 500, + title: title, + type: type, + detail: detail, + instance: instance); + + return new ObjectResult(problemDetails); + } + /// /// Creates an that produces a response. /// @@ -1837,31 +1889,64 @@ namespace Microsoft.AspNetCore.Mvc } /// - /// Creates an that produces a response. + /// Creates an that produces a response + /// with validation errors from . /// + /// The . /// The created for the response. [NonAction] public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary) - { - if (modelStateDictionary == null) - { - throw new ArgumentNullException(nameof(modelStateDictionary)); - } + => ValidationProblem(detail: null, modelStateDictionary: modelStateDictionary); - var validationProblem = new ValidationProblemDetails(modelStateDictionary); - return new BadRequestObjectResult(validationProblem); - } /// - /// Creates an that produces a response + /// Creates an that produces a response /// with validation errors from . /// - /// The created for the response. + /// The created for the response. [NonAction] public virtual ActionResult ValidationProblem() + => ValidationProblem(ModelState); + + /// + /// Creates an that produces a response + /// with a value. + /// + /// The value for . + /// The value for . + /// The status code. + /// The value for . + /// The value for . + /// The . + /// When uses . + /// The created for the response. + [NonAction] + public virtual ActionResult ValidationProblem( + string detail = null, + string instance = null, + int? statusCode = null, + string title = null, + string type = null, + [ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null) { - var validationProblem = new ValidationProblemDetails(ModelState); - return new BadRequestObjectResult(validationProblem); + modelStateDictionary ??= ModelState; + + var validationProblem = ProblemDetailsFactory.CreateValidationProblemDetails( + HttpContext, + modelStateDictionary, + statusCode: statusCode, + title: title, + type: type, + detail: detail, + instance: instance); + + if (validationProblem.Status == 400) + { + // For compatibility with 2.x, continue producing BadRequestObjectResult instances if the status code is 400. + return new BadRequestObjectResult(validationProblem); + } + + return new ObjectResult(validationProblem); } /// diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs index 3ed6a3eeef..4cbd1a2dba 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.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 Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -10,13 +9,9 @@ using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection { - internal class ApiBehaviorOptionsSetup : - IConfigureOptions, - IPostConfigureOptions + internal class ApiBehaviorOptionsSetup : IConfigureOptions { - internal static readonly Func DefaultFactory = DefaultInvalidModelStateResponse; - internal static readonly Func ProblemDetailsFactory = - ProblemDetailsInvalidModelStateResponse; + private ProblemDetailsFactory _problemDetailsFactory; public void Configure(ApiBehaviorOptions options) { @@ -25,20 +20,34 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(options)); } - options.InvalidModelStateResponseFactory = DefaultFactory; + options.InvalidModelStateResponseFactory = context => + { + // ProblemDetailsFactory depends on the ApiBehaviorOptions instance. We intentionally avoid constructor injecting + // it in this options setup to to avoid a DI cycle. + _problemDetailsFactory ??= context.HttpContext.RequestServices.GetRequiredService(); + return ProblemDetailsInvalidModelStateResponse(_problemDetailsFactory, context); + }; + ConfigureClientErrorMapping(options); } - public void PostConfigure(string name, ApiBehaviorOptions options) + internal static IActionResult ProblemDetailsInvalidModelStateResponse(ProblemDetailsFactory problemDetailsFactory, ActionContext context) { - // We want to use problem details factory only if - // (a) it has not been opted out of (SuppressMapClientErrors = true) - // (b) a different factory was configured - if (!options.SuppressMapClientErrors && - object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory)) + var problemDetails = problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState); + ObjectResult result; + if (problemDetails.Status == 400) { - options.InvalidModelStateResponseFactory = ProblemDetailsFactory; + // For compatibility with 2.x, continue producing BadRequestObjectResult instances if the status code is 400. + result = new BadRequestObjectResult(problemDetails); } + else + { + result = new ObjectResult(problemDetails); + } + result.ContentTypes.Add("application/problem+json"); + result.ContentTypes.Add("application/problem+xml"); + + return result; } // Internal for unit testing @@ -91,33 +100,12 @@ namespace Microsoft.Extensions.DependencyInjection Link = "https://tools.ietf.org/html/rfc4918#section-11.2", Title = Resources.ApiConventions_Title_422, }; - } - private static IActionResult DefaultInvalidModelStateResponse(ActionContext context) - { - var result = new BadRequestObjectResult(context.ModelState); - - result.ContentTypes.Add("application/json"); - result.ContentTypes.Add("application/xml"); - - return result; - } - - internal static IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context) - { - var problemDetails = new ValidationProblemDetails(context.ModelState) + options.ClientErrorMapping[500] = new ClientErrorData { - Status = StatusCodes.Status400BadRequest, + Link = "https://tools.ietf.org/html/rfc7231#section-6.6.1", + Title = Resources.ApiConventions_Title_500, }; - - ProblemDetailsClientErrorFactory.SetTraceId(context, problemDetails); - - var result = new BadRequestObjectResult(problemDetails); - - result.ContentTypes.Add("application/problem+json"); - result.ContentTypes.Add("application/problem+xml"); - - return result; } } } diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index e34f74c221..82181cd034 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -149,8 +149,6 @@ namespace Microsoft.Extensions.DependencyInjection ServiceDescriptor.Transient, MvcCoreMvcOptionsSetup>()); services.TryAddEnumerable( ServiceDescriptor.Transient, ApiBehaviorOptionsSetup>()); - services.TryAddEnumerable( - ServiceDescriptor.Transient, ApiBehaviorOptionsSetup>()); services.TryAddEnumerable( ServiceDescriptor.Transient, MvcCoreRouteOptionsSetup>()); @@ -260,6 +258,7 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton, ContentResultExecutor>(); services.TryAddSingleton, SystemTextJsonResultExecutor>(); services.TryAddSingleton(); + services.TryAddSingleton(); // // Route Handlers diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs new file mode 100644 index 0000000000..cbd1a2aad4 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs @@ -0,0 +1,97 @@ +// 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.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + internal sealed class DefaultProblemDetailsFactory : ProblemDetailsFactory + { + private readonly ApiBehaviorOptions _options; + + public DefaultProblemDetailsFactory(IOptions options) + { + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + } + + public override ProblemDetails CreateProblemDetails( + HttpContext httpContext, + int? statusCode = null, + string title = null, + string type = null, + string detail = null, + string instance = null) + { + statusCode ??= 500; + + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = title, + Type = type, + Detail = detail, + Instance = instance, + }; + + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + + return problemDetails; + } + + public override ValidationProblemDetails CreateValidationProblemDetails( + HttpContext httpContext, + ModelStateDictionary modelStateDictionary, + int? statusCode = null, + string title = null, + string type = null, + string detail = null, + string instance = null) + { + if (modelStateDictionary == null) + { + throw new ArgumentNullException(nameof(modelStateDictionary)); + } + + statusCode ??= 400; + + var problemDetails = new ValidationProblemDetails(modelStateDictionary) + { + Status = statusCode, + Type = type, + Detail = detail, + Instance = instance, + }; + + if (title != null) + { + // For validation problem details, don't overwrite the default title with null. + problemDetails.Title = title; + } + + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + + return problemDetails; + } + + private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode) + { + problemDetails.Status ??= statusCode; + + if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData)) + { + problemDetails.Title ??= clientErrorData.Title; + problemDetails.Type ??= clientErrorData.Link; + } + + var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; + if (traceId != null) + { + problemDetails.Extensions["traceId"] = traceId; + } + } + } +} diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index ff47a18fa6..f0fe9eadaf 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -2,37 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Diagnostics; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.Infrastructure { internal class ProblemDetailsClientErrorFactory : IClientErrorFactory { - private static readonly string TraceIdentifierKey = "traceId"; - private readonly ApiBehaviorOptions _options; + private readonly ProblemDetailsFactory _problemDetailsFactory; - public ProblemDetailsClientErrorFactory(IOptions options) + public ProblemDetailsClientErrorFactory(ProblemDetailsFactory problemDetailsFactory) { - _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); } 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; - - SetTraceId(actionContext, problemDetails); - } + var problemDetails = _problemDetailsFactory.CreateProblemDetails(actionContext.HttpContext, clientError.StatusCode); return new ObjectResult(problemDetails) { @@ -44,11 +28,5 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure }, }; } - - internal static void SetTraceId(ActionContext actionContext, ProblemDetails problemDetails) - { - var traceId = Activity.Current?.Id ?? actionContext.HttpContext.TraceIdentifier; - problemDetails.Extensions[TraceIdentifierKey] = traceId; - } } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsFactory.cs new file mode 100644 index 0000000000..aeb930dfb5 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsFactory.cs @@ -0,0 +1,52 @@ +// 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.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + /// + /// Factory to produce and . + /// + public abstract class ProblemDetailsFactory + { + /// + /// Creates a instance that configures defaults based on values specified in . + /// + /// The . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The instance. + public abstract ProblemDetails CreateProblemDetails( + HttpContext httpContext, + int? statusCode = null, + string title = null, + string type = null, + string detail = null, + string instance = null); + + /// + /// Creates a instance that configures defaults based on values specified in . + /// + /// The . + /// The . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The instance. + public abstract ValidationProblemDetails CreateValidationProblemDetails( + HttpContext httpContext, + ModelStateDictionary modelStateDictionary, + int? statusCode = null, + string title = null, + string type = null, + string detail = null, + string instance = null); + } +} diff --git a/src/Mvc/Mvc.Core/src/Resources.resx b/src/Mvc/Mvc.Core/src/Resources.resx index a255bcf725..03647f8e23 100644 --- a/src/Mvc/Mvc.Core/src/Resources.resx +++ b/src/Mvc/Mvc.Core/src/Resources.resx @@ -510,4 +510,7 @@ Unexcepted end when reading JSON. + + An error occured while processing your request. + \ No newline at end of file diff --git a/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs b/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs index 89a38e587a..8c3a4b17c6 100644 --- a/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs +++ b/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs @@ -9,8 +9,11 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Routing; @@ -2187,7 +2190,6 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test { // Arrange var contentController = new ContentController(); - var expected = MediaTypeHeaderValue.Parse("text/plain; charset=utf-8"); // Act var contentResult = (ContentResult)contentController.Content_WithNoEncoding(); @@ -2290,6 +2292,163 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test Assert.Equal(statusCode, result.StatusCode); } + [Fact] + public void ValidationProblemDetails_Works() + { + // Arrange + var context = new ControllerContext(new ActionContext( + new DefaultHttpContext { TraceIdentifier = "some-trace" }, + new RouteData(), + new ControllerActionDescriptor())); + + context.ModelState.AddModelError("key1", "error1"); + + var options = GetApiBehaviorOptions(); + var controller = new TestableController + { + ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)), + ControllerContext = context, + }; + + // Act + var actionResult = controller.ValidationProblem(); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + var problemDetails = Assert.IsType(badRequestResult.Value); + Assert.Equal(400, problemDetails.Status); + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + Assert.Equal("some-trace", problemDetails.Extensions["traceId"]); + Assert.Equal(new[] { "error1" }, problemDetails.Errors["key1"]); + } + + [Fact] + public void ValidationProblemDetails_UsesSpecifiedTitle() + { + // Arrange + var detail = "My detail"; + var title = "Custom title"; + var type = "http://custom-link"; + var options = GetApiBehaviorOptions(); + + var controller = new TestableController + { + ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)), + }; + + // Act + var actionResult = controller.ValidationProblem(detail: detail, title: title, type: type); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + var problemDetails = Assert.IsType(badRequestResult.Value); + Assert.Equal(title, problemDetails.Title); + Assert.Equal(type, problemDetails.Type); + Assert.Equal(detail, problemDetails.Detail); + } + + [Fact] + public void ProblemDetails_Works() + { + // Arrange + var context = new ControllerContext(new ActionContext( + new DefaultHttpContext { TraceIdentifier = "some-trace" }, + new RouteData(), + new ControllerActionDescriptor())); + + var options = GetApiBehaviorOptions(); + + var controller = new TestableController + { + ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)), + ControllerContext = context, + }; + + // Act + var actionResult = controller.Problem(); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + var problemDetails = Assert.IsType(badRequestResult.Value); + Assert.Equal(500, problemDetails.Status); + Assert.Equal("An error occured while processing your request.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type); + Assert.Equal("some-trace", problemDetails.Extensions["traceId"]); + } + + [Fact] + public void ProblemDetails_UsesPassedInValues() + { + // Arrange + var title = "The website is down."; + var detail = "Try again in a few minutes."; + var options = GetApiBehaviorOptions(); + + var controller = new TestableController + { + ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)), + }; + + // Act + var actionResult = controller.Problem(detail, title: title); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + var problemDetails = Assert.IsType(badRequestResult.Value); + Assert.Equal(500, problemDetails.Status); + Assert.Equal(title, problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type); + Assert.Equal(detail, problemDetails.Detail); + } + + [Fact] + public void ProblemDetails_UsesPassedInStatusCode() + { + // Arrange + var options = GetApiBehaviorOptions(); + + var controller = new TestableController + { + ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)), + }; + + // Act + var actionResult = controller.Problem(statusCode: 422); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + var problemDetails = Assert.IsType(badRequestResult.Value); + Assert.Equal(422, problemDetails.Status); + Assert.Equal("Unprocessable entity.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc4918#section-11.2", problemDetails.Type); + } + + private static ApiBehaviorOptions GetApiBehaviorOptions() + { + return new ApiBehaviorOptions + { + ClientErrorMapping = + { + [400] = new ClientErrorData + { + Title = "One or more validation errors occurred.", + Link = "https://tools.ietf.org/html/rfc7231#section-6.5.1" + }, + [422] = new ClientErrorData + { + Title = "Unprocessable entity.", + Link = "https://tools.ietf.org/html/rfc4918#section-11.2" + }, + [500] = new ClientErrorData + { + Title = "An error occured while processing your request.", + Link = "https://tools.ietf.org/html/rfc7231#section-6.6.1" + } + } + }; + } + public static IEnumerable RedirectTestData { get diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs index 82a137bf87..ebca639ee9 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs @@ -6,31 +6,18 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Xunit; namespace Microsoft.Extensions.DependencyInjection { public class ApiBehaviorOptionsSetupTest { - [Fact] - public void Configure_AssignsInvalidModelStateResponseFactory() - { - // Arrange - var optionsSetup = new ApiBehaviorOptionsSetup(); - var options = new ApiBehaviorOptions(); - - // Act - optionsSetup.Configure(options); - - // Assert - Assert.Same(ApiBehaviorOptionsSetup.DefaultFactory, options.InvalidModelStateResponseFactory); - } - [Fact] public void Configure_AddsClientErrorMappings() { // Arrange - var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, }; + var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, 500, }; var optionsSetup = new ApiBehaviorOptionsSetup(); var options = new ApiBehaviorOptions(); @@ -41,50 +28,15 @@ namespace Microsoft.Extensions.DependencyInjection Assert.Equal(expected, options.ClientErrorMapping.Keys); } - [Fact] - public void PostConfigure_SetProblemDetailsModelStateResponseFactory() - { - // Arrange - var optionsSetup = new ApiBehaviorOptionsSetup(); - var options = new ApiBehaviorOptions(); - - // Act - optionsSetup.Configure(options); - optionsSetup.PostConfigure(string.Empty, options); - - // Assert - Assert.Same(ApiBehaviorOptionsSetup.ProblemDetailsFactory, options.InvalidModelStateResponseFactory); - } - - [Fact] - public void PostConfigure_DoesNotSetProblemDetailsFactory_IfValueWasModified() - { - // Arrange - var optionsSetup = new ApiBehaviorOptionsSetup(); - var options = new ApiBehaviorOptions(); - Func expected = _ => null; - - // Act - optionsSetup.Configure(options); - // This is equivalent to user code updating the value via ConfigureOptions - options.InvalidModelStateResponseFactory = expected; - optionsSetup.PostConfigure(string.Empty, options); - - // Assert - Assert.Same(expected, options.InvalidModelStateResponseFactory); - } - [Fact] public void ProblemDetailsInvalidModelStateResponse_ReturnsBadRequestWithProblemDetails() { // Arrange - var actionContext = new ActionContext - { - HttpContext = new DefaultHttpContext { TraceIdentifier = "42" }, - }; + var actionContext = GetActionContext(); + var factory = GetProblemDetailsFactory(); // Act - var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext); + var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext); // Assert var badRequest = Assert.IsType(result); @@ -92,6 +44,30 @@ namespace Microsoft.Extensions.DependencyInjection var problemDetails = Assert.IsType(badRequest.Value); Assert.Equal(400, problemDetails.Status); + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + } + + [Fact] + public void ProblemDetailsInvalidModelStateResponse_UsesUserConfiguredLink() + { + // Arrange + var link = "http://mylink"; + var actionContext = GetActionContext(); + + var factory = GetProblemDetailsFactory(options => options.ClientErrorMapping[400].Link = link); + + // Act + var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext); + + // Assert + var badRequest = Assert.IsType(result); + Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, badRequest.ContentTypes.OrderBy(c => c)); + + var problemDetails = Assert.IsType(badRequest.Value); + Assert.Equal(400, problemDetails.Status); + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal(link, problemDetails.Type); } [Fact] @@ -100,13 +76,11 @@ namespace Microsoft.Extensions.DependencyInjection // Arrange using (new ActivityReplacer()) { - var actionContext = new ActionContext - { - HttpContext = new DefaultHttpContext { TraceIdentifier = "42" }, - }; + var actionContext = GetActionContext(); + var factory = GetProblemDetailsFactory(); // Act - var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext); + var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext); // Assert var badRequest = Assert.IsType(result); @@ -119,18 +93,38 @@ namespace Microsoft.Extensions.DependencyInjection public void ProblemDetailsInvalidModelStateResponse_SetsTraceIdFromRequest_IfActivityIsNull() { // Arrange - var actionContext = new ActionContext - { - HttpContext = new DefaultHttpContext { TraceIdentifier = "42" }, - }; + var actionContext = GetActionContext(); + var factory = GetProblemDetailsFactory(); // Act - var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext); + var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext); // Assert var badRequest = Assert.IsType(result); var problemDetails = Assert.IsType(badRequest.Value); Assert.Equal("42", problemDetails.Extensions["traceId"]); } + + private static ProblemDetailsFactory GetProblemDetailsFactory(Action configure = null) + { + var options = new ApiBehaviorOptions(); + var setup = new ApiBehaviorOptionsSetup(); + + setup.Configure(options); + if (configure != null) + { + configure(options); + } + + return new DefaultProblemDetailsFactory(Options.Options.Create(options)); + } + + private static ActionContext GetActionContext() + { + return new ActionContext + { + HttpContext = new DefaultHttpContext { TraceIdentifier = "42" }, + }; + } } } diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs index 1c9117ef6e..612129f8c2 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs @@ -264,13 +264,6 @@ namespace Microsoft.AspNetCore.Mvc typeof(ApiBehaviorOptionsSetup), } }, - { - typeof(IPostConfigureOptions), - new Type[] - { - typeof(ApiBehaviorOptionsSetup), - } - }, { typeof(IActionConstraintProvider), new Type[] diff --git a/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs b/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs index 4f8948aa62..6206aeab62 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs @@ -1,8 +1,8 @@ // 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.Collections.Generic; using System.IO; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs new file mode 100644 index 0000000000..0bf84da14f --- /dev/null +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs @@ -0,0 +1,188 @@ +// 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.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + public class ProblemDetailsFactoryTest + { + private readonly ProblemDetailsFactory Factory = GetProblemDetails(); + + [Fact] + public void CreateProblemDetails_DefaultValues() + { + // Act + var problemDetails = Factory.CreateProblemDetails(GetHttpContext()); + + // Assert + Assert.Equal(500, problemDetails.Status); + Assert.Equal("An error occured while processing your request.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type); + Assert.Null(problemDetails.Instance); + Assert.Null(problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + } + + [Fact] + public void CreateProblemDetails_WithStatusCode() + { + // Act + var problemDetails = Factory.CreateProblemDetails(GetHttpContext(), statusCode: 406); + + // Assert + Assert.Equal(406, problemDetails.Status); + Assert.Equal("Not Acceptable", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.6", problemDetails.Type); + Assert.Null(problemDetails.Instance); + Assert.Null(problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + } + + [Fact] + public void CreateProblemDetails_WithDetailAndTitle() + { + // Act + var title = "Some title"; + var detail = "some detail"; + var problemDetails = Factory.CreateProblemDetails(GetHttpContext(), statusCode: 406, title: title, detail: detail); + + // Assert + Assert.Equal(406, problemDetails.Status); + Assert.Equal(title, problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.6", problemDetails.Type); + Assert.Null(problemDetails.Instance); + Assert.Equal(detail, problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + } + + [Fact] + public void CreateValidationProblemDetails_DefaultValues() + { + // Act + var modelState = new ModelStateDictionary(); + modelState.AddModelError("some-key", "some-value"); + var problemDetails = Factory.CreateValidationProblemDetails(GetHttpContext(), modelState); + + // Assert + Assert.Equal(400, problemDetails.Status); + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + Assert.Null(problemDetails.Instance); + Assert.Null(problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + Assert.Collection( + problemDetails.Errors, + kvp => + { + Assert.Equal("some-key", kvp.Key); + Assert.Equal(new[] { "some-value" }, kvp.Value); + }); + } + + [Fact] + public void CreateValidationProblemDetails_WithStatusCode() + { + // Act + var modelState = new ModelStateDictionary(); + modelState.AddModelError("some-key", "some-value"); + var problemDetails = Factory.CreateValidationProblemDetails(GetHttpContext(), modelState, 422); + + // Assert + Assert.Equal(422, problemDetails.Status); + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc4918#section-11.2", problemDetails.Type); + Assert.Null(problemDetails.Instance); + Assert.Null(problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + Assert.Collection( + problemDetails.Errors, + kvp => + { + Assert.Equal("some-key", kvp.Key); + Assert.Equal(new[] { "some-value" }, kvp.Value); + }); + } + + [Fact] + public void CreateValidationProblemDetails_WithTitleAndInstance() + { + // Act + var title = "Some title"; + var instance = "some instance"; + var modelState = new ModelStateDictionary(); + modelState.AddModelError("some-key", "some-value"); + var problemDetails = Factory.CreateValidationProblemDetails(GetHttpContext(), modelState, title: title, instance: instance); + + // Assert + Assert.Equal(400, problemDetails.Status); + Assert.Equal(title, problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + Assert.Equal(instance, problemDetails.Instance); + Assert.Null(problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + Assert.Collection( + problemDetails.Errors, + kvp => + { + Assert.Equal("some-key", kvp.Key); + Assert.Equal(new[] { "some-value" }, kvp.Value); + }); + } + + private static DefaultHttpContext GetHttpContext() + { + return new DefaultHttpContext + { + TraceIdentifier = "some-trace", + }; + } + + private static ProblemDetailsFactory GetProblemDetails() + { + var options = new ApiBehaviorOptions(); + new ApiBehaviorOptionsSetup().Configure(options); + return new DefaultProblemDetailsFactory(Options.Create(options)); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs index 3a84deabe7..93ac451d37 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs @@ -15,13 +15,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure { // Arrange var clientError = new UnsupportedMediaTypeResult(); - var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions + var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions { ClientErrorMapping = { [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -31,7 +32,6 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure 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); @@ -42,13 +42,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure { // Arrange var clientError = new UnsupportedMediaTypeResult(); - var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions + var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions { ClientErrorMapping = { [415] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -71,13 +72,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure using (new ActivityReplacer()) { var clientError = new UnsupportedMediaTypeResult(); - var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions + var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions { ClientErrorMapping = { - [415] = new ClientErrorData { Link = "Some link", Title = "Summary" }, + [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -96,13 +98,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure { // Arrange var clientError = new UnsupportedMediaTypeResult(); - var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions + var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions { ClientErrorMapping = { - [415] = new ClientErrorData { Link = "Some link", Title = "Summary" }, + [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); diff --git a/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs b/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs index d7c6326ace..ca92aba1b6 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs @@ -56,6 +56,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { Converters = { new ValidationProblemDetailsConverter() } }); + + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + Assert.Collection( problemDetails.Errors.OrderBy(kvp => kvp.Key), kvp => diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs index ff911cc981..66feb4d7cf 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs @@ -24,15 +24,11 @@ namespace BasicWebSite services.Configure(options => { - var previous = options.InvalidModelStateResponseFactory; options.InvalidModelStateResponseFactory = context => { - var result = (BadRequestObjectResult)previous(context); - if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute)) - { - result.ContentTypes.Clear(); - result.ContentTypes.Add("application/vnd.error+json"); - } + var result = new BadRequestObjectResult(context.ModelState); + result.ContentTypes.Clear(); + result.ContentTypes.Add("application/vnd.error+json"); return result; };