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);
+ });
+ }
+ }
+}