diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs index 084884cc5e..3bc7f57385 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs @@ -1468,6 +1468,49 @@ namespace Microsoft.AspNetCore.Mvc return new BadRequestObjectResult(modelState); } + /// + /// Creates an that produces a response. + /// + /// The created for the response. + [NonAction] + public virtual BadRequestObjectResult ValidationProblem(ValidationProblemDescription descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return new BadRequestObjectResult(descriptor); + } + + /// + /// Creates an that produces a response. + /// + /// The created for the response. + [NonAction] + public virtual BadRequestObjectResult ValidationProblem(ModelStateDictionary modelStateDictionary) + { + if (modelStateDictionary == null) + { + throw new ArgumentNullException(nameof(modelStateDictionary)); + } + + var validationProblem = new ValidationProblemDescription(modelStateDictionary); + return new BadRequestObjectResult(validationProblem); + } + + /// + /// Creates an that produces a response + /// with validation errors from . + /// + /// The created for the response. + [NonAction] + public virtual BadRequestObjectResult ValidationProblem() + { + var validationProblem = new ValidationProblemDescription(ModelState); + return new BadRequestObjectResult(validationProblem); + } + /// /// Creates a object that produces a response. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ProblemDescription.cs b/src/Microsoft.AspNetCore.Mvc.Core/ProblemDescription.cs new file mode 100644 index 0000000000..dedd6a0c48 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ProblemDescription.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 System.Collections.Generic; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807. + /// + public class ProblemDescription + { + /// + /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when + /// dereferenced, it provide human-readable documentation for the problem type + /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be + /// "about:blank". + /// + public string Type { get; set; } + + /// + /// 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). + /// + public string Title { get; set; } + + /// + /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. + /// + public int? Status { get; set; } + + /// + /// A human-readable explanation specific to this occurrence of the problem. + /// + 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. + /// + public string Instance { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs index 48d1e9674e..f424455c65 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs @@ -1326,6 +1326,20 @@ namespace Microsoft.AspNetCore.Mvc.Core internal static string FormatUrlHelper_RelativePagePathIsNotSupported(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("UrlHelper_RelativePagePathIsNotSupported"), p0); + /// + /// One or more validation errors occured. + /// + internal static string ValidationProblemDescription_Title + { + get => GetString("ValidationProblemDescription_Title"); + } + + /// + /// One or more validation errors occured. + /// + internal static string FormatValidationProblemDescription_Title() + => GetString("ValidationProblemDescription_Title"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx index 2202478c35..fb875f3667 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx @@ -412,4 +412,7 @@ The relative page path '{0}' can only be used while executing a Razor Page. Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page. + + One or more validation errors occured. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ValidationProblemDescription.cs b/src/Microsoft.AspNetCore.Mvc.Core/ValidationProblemDescription.cs new file mode 100644 index 0000000000..610707566a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ValidationProblemDescription.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// A for validation errors. + /// + public class ValidationProblemDescription : ProblemDescription + { + /// + /// Intializes a new instance of . + /// + public ValidationProblemDescription() + { + Title = Resources.ValidationProblemDescription_Title; + } + + public ValidationProblemDescription(ModelStateDictionary modelState) + : this() + { + if (modelState == null) + { + throw new ArgumentNullException(nameof(modelState)); + } + + foreach (var keyModelStatePair in modelState) + { + var key = keyModelStatePair.Key; + var errors = keyModelStatePair.Value.Errors; + if (errors != null && errors.Count > 0) + { + if (errors.Count == 1) + { + var errorMessage = GetErrorMessage(errors[0]); + Errors.Add(key, new[] { errorMessage }); + } + else + { + var errorMessages = new string[errors.Count]; + for (var i = 0; i < errors.Count; i++) + { + errorMessages[i] = GetErrorMessage(errors[i]); + } + + Errors.Add(key, errorMessages); + } + } + } + + string GetErrorMessage(ModelError error) + { + return string.IsNullOrEmpty(error.ErrorMessage) ? + Resources.SerializableError_DefaultError : + error.ErrorMessage; + } + } + + /// + /// Gets or sets the validation errors associated with this instance of . + /// + public IDictionary Errors { get; } = new Dictionary(StringComparer.Ordinal); + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ValidationProblemDescriptionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ValidationProblemDescriptionTest.cs new file mode 100644 index 0000000000..7d6e062005 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ValidationProblemDescriptionTest.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class ValidationProblemDescriptionTest + { + [Fact] + public void Constructor_SetsTitle() + { + // Arrange & Act + var problemDescription = new ValidationProblemDescription(); + + // Assert + Assert.Equal("One or more validation errors occured.", problemDescription.Title); + Assert.Empty(problemDescription.Errors); + } + + [Fact] + public void Constructor_SerializesErrorsFromModelStateDictionary() + { + // Arrange + var modelStateDictionary = new ModelStateDictionary(); + modelStateDictionary.AddModelError("key1", "error1"); + modelStateDictionary.SetModelValue("key2", new object(), "value"); + modelStateDictionary.AddModelError("key3", "error2"); + modelStateDictionary.AddModelError("key3", "error3"); + + // Act + var problemDescription = new ValidationProblemDescription(modelStateDictionary); + + // Assert + Assert.Equal("One or more validation errors occured.", problemDescription.Title); + Assert.Collection( + problemDescription.Errors, + item => + { + Assert.Equal("key1", item.Key); + Assert.Equal(new[] { "error1" }, item.Value); + }, + item => + { + Assert.Equal("key3", item.Key); + Assert.Equal(new[] { "error2", "error3" }, item.Value); + }); + } + } +}