diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/HttpValidationProblemDetails.cs b/src/Http/Http.Abstractions/src/ProblemDetails/HttpValidationProblemDetails.cs
index 99d07781b92a..430a4dc279b1 100644
--- a/src/Http/Http.Abstractions/src/ProblemDetails/HttpValidationProblemDetails.cs
+++ b/src/Http/Http.Abstractions/src/ProblemDetails/HttpValidationProblemDetails.cs
@@ -24,6 +24,15 @@ public HttpValidationProblemDetails()
///
/// The validation errors.
public HttpValidationProblemDetails(IDictionary errors)
+ : this((IEnumerable>)errors)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of using the specified .
+ ///
+ /// The validation errors.
+ public HttpValidationProblemDetails(IEnumerable> errors)
: this(new Dictionary(errors ?? throw new ArgumentNullException(nameof(errors)), StringComparer.Ordinal))
{
}
diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
index 5c05a9b9562b..b08a98e0390c 100644
--- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
@@ -3,3 +3,4 @@
Microsoft.AspNetCore.Http.HostString.HostString(string? value) -> void
*REMOVED*Microsoft.AspNetCore.Http.HostString.Value.get -> string!
Microsoft.AspNetCore.Http.HostString.Value.get -> string?
+Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IEnumerable>! errors) -> void
diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..61c6112ae413 100644
--- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
@@ -1 +1,2 @@
#nullable enable
+Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IEnumerable>! errors) -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions)
diff --git a/src/Http/Http.Results/src/PublicAPI.Unshipped.txt b/src/Http/Http.Results/src/PublicAPI.Unshipped.txt
index 83875c87a8b5..c68c37c4f19c 100644
--- a/src/Http/Http.Results/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Results/src/PublicAPI.Unshipped.txt
@@ -8,5 +8,17 @@ Microsoft.AspNetCore.Http.HttpResults.InternalServerError.StatusCode.get
Microsoft.AspNetCore.Http.HttpResults.InternalServerError.Value.get -> TValue?
static Microsoft.AspNetCore.Http.Results.InternalServerError() -> Microsoft.AspNetCore.Http.IResult!
static Microsoft.AspNetCore.Http.Results.InternalServerError(TValue? error) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Problem(string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null, System.Collections.Generic.IEnumerable>? extensions = null) -> Microsoft.AspNetCore.Http.IResult!
+*REMOVED*static Microsoft.AspNetCore.Http.Results.Problem(string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null, System.Collections.Generic.IDictionary? extensions = null) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Problem(string? detail, string? instance, int? statusCode, string? title, string? type, System.Collections.Generic.IDictionary? extensions) -> Microsoft.AspNetCore.Http.IResult!
+*REMOVED*static Microsoft.AspNetCore.Http.Results.ValidationProblem(System.Collections.Generic.IDictionary! errors, string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null, System.Collections.Generic.IDictionary? extensions = null) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.ValidationProblem(System.Collections.Generic.IDictionary! errors, string? detail, string? instance, int? statusCode, string? title, string? type, System.Collections.Generic.IDictionary? extensions) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.ValidationProblem(System.Collections.Generic.IEnumerable>! errors, string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null, System.Collections.Generic.IEnumerable>? extensions = null) -> Microsoft.AspNetCore.Http.IResult!
static Microsoft.AspNetCore.Http.TypedResults.InternalServerError() -> Microsoft.AspNetCore.Http.HttpResults.InternalServerError!
-static Microsoft.AspNetCore.Http.TypedResults.InternalServerError(TValue? error) -> Microsoft.AspNetCore.Http.HttpResults.InternalServerError!
\ No newline at end of file
+static Microsoft.AspNetCore.Http.TypedResults.InternalServerError(TValue? error) -> Microsoft.AspNetCore.Http.HttpResults.InternalServerError!
+static Microsoft.AspNetCore.Http.TypedResults.Problem(string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null, System.Collections.Generic.IEnumerable>? extensions = null) -> Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult!
+*REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Problem(string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null, System.Collections.Generic.IDictionary? extensions = null) -> Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult!
+static Microsoft.AspNetCore.Http.TypedResults.Problem(string? detail, string? instance, int? statusCode, string? title, string? type, System.Collections.Generic.IDictionary? extensions) -> Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult!
+*REMOVED*static Microsoft.AspNetCore.Http.TypedResults.ValidationProblem(System.Collections.Generic.IDictionary! errors, string? detail = null, string? instance = null, string? title = null, string? type = null, System.Collections.Generic.IDictionary? extensions = null) -> Microsoft.AspNetCore.Http.HttpResults.ValidationProblem!
+static Microsoft.AspNetCore.Http.TypedResults.ValidationProblem(System.Collections.Generic.IDictionary! errors, string? detail, string? instance, string? title, string? type, System.Collections.Generic.IDictionary? extensions) -> Microsoft.AspNetCore.Http.HttpResults.ValidationProblem!
+static Microsoft.AspNetCore.Http.TypedResults.ValidationProblem(System.Collections.Generic.IEnumerable>! errors, string? detail = null, string? instance = null, string? title = null, string? type = null, System.Collections.Generic.IEnumerable>? extensions = null) -> Microsoft.AspNetCore.Http.HttpResults.ValidationProblem!
diff --git a/src/Http/Http.Results/src/Results.cs b/src/Http/Http.Results/src/Results.cs
index a033e10d2af4..662fe0769cce 100644
--- a/src/Http/Http.Results/src/Results.cs
+++ b/src/Http/Http.Results/src/Results.cs
@@ -712,12 +712,33 @@ public static IResult InternalServerError(TValue? error)
/// The value for .
/// The created for the response.
public static IResult Problem(
+ string? detail,
+ string? instance,
+ int? statusCode,
+ string? title,
+ string? type,
+ IDictionary? extensions)
+ => TypedResults.Problem(detail, instance, statusCode, title, type, extensions);
+
+ ///
+ /// Produces a response.
+ ///
+ /// The value for .
+ /// The value for .
+ /// The value for .
+ /// The value for .
+ /// The value for .
+ /// The value for .
+ /// The created for the response.
+#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
+ public static IResult Problem(
+#pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
string? detail = null,
string? instance = null,
int? statusCode = null,
string? title = null,
string? type = null,
- IDictionary? extensions = null)
+ IEnumerable>? extensions = null)
=> TypedResults.Problem(detail, instance, statusCode, title, type, extensions);
///
@@ -742,13 +763,41 @@ public static IResult Problem(ProblemDetails problemDetails)
/// The created for the response.
public static IResult ValidationProblem(
IDictionary errors,
+ string? detail,
+ string? instance,
+ int? statusCode,
+ string? title,
+ string? type,
+ IDictionary? extensions)
+ {
+ return ValidationProblem(errors, detail, instance, statusCode, title, type, (IEnumerable>?)extensions);
+ }
+
+ ///
+ /// Produces a response
+ /// with a value.
+ ///
+ /// One or more validation errors.
+ /// The value for .
+ /// The value for .
+ /// The status code.
+ /// The value for . Defaults to "One or more validation errors occurred."
+ /// The value for .
+ /// The value for .
+ /// The created for the response.
+#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
+ public static IResult ValidationProblem(
+#pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
+ IEnumerable> errors,
string? detail = null,
string? instance = null,
int? statusCode = null,
string? title = null,
string? type = null,
- IDictionary? extensions = null)
+ IEnumerable>? extensions = null)
{
+ ArgumentNullException.ThrowIfNull(errors);
+
// TypedResults.ValidationProblem() does not allow setting the statusCode so we do this manually here
var problemDetails = new HttpValidationProblemDetails(errors)
{
@@ -765,7 +814,7 @@ public static IResult ValidationProblem(
return TypedResults.Problem(problemDetails);
}
- private static void CopyExtensions(IDictionary? extensions, HttpValidationProblemDetails problemDetails)
+ private static void CopyExtensions(IEnumerable>? extensions, HttpValidationProblemDetails problemDetails)
{
if (extensions is not null)
{
diff --git a/src/Http/Http.Results/src/TypedResults.cs b/src/Http/Http.Results/src/TypedResults.cs
index 95584ec96e45..8eb7c9fcd834 100644
--- a/src/Http/Http.Results/src/TypedResults.cs
+++ b/src/Http/Http.Results/src/TypedResults.cs
@@ -755,12 +755,35 @@ public static StatusCodeHttpResult StatusCode(int statusCode)
/// The value for .
/// The created for the response.
public static ProblemHttpResult Problem(
+ string? detail,
+ string? instance,
+ int? statusCode,
+ string? title,
+ string? type,
+ IDictionary? extensions)
+ {
+ return Problem(detail, instance, statusCode, title, type, (IEnumerable>?)extensions);
+ }
+
+ ///
+ /// Produces a response.
+ ///
+ /// The value for .
+ /// The value for .
+ /// The value for .
+ /// The value for .
+ /// The value for .
+ /// The value for .
+ /// The created for the response.
+#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
+ public static ProblemHttpResult Problem(
+#pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
string? detail = null,
string? instance = null,
int? statusCode = null,
string? title = null,
string? type = null,
- IDictionary? extensions = null)
+ IEnumerable>? extensions = null)
{
var problemDetails = new ProblemDetails
{
@@ -776,17 +799,6 @@ public static ProblemHttpResult Problem(
return new(problemDetails);
}
- private static void CopyExtensions(IDictionary? extensions, ProblemDetails problemDetails)
- {
- if (extensions is not null)
- {
- foreach (var extension in extensions)
- {
- problemDetails.Extensions.Add(extension);
- }
- }
- }
-
///
/// Produces a response.
///
@@ -811,11 +823,34 @@ public static ProblemHttpResult Problem(ProblemDetails problemDetails)
/// The created for the response.
public static ValidationProblem ValidationProblem(
IDictionary errors,
+ string? detail,
+ string? instance,
+ string? title,
+ string? type,
+ IDictionary? extensions)
+ {
+ return ValidationProblem(errors, detail, instance, title, type, (IEnumerable>?)extensions);
+ }
+
+ ///
+ /// Produces a response with an value.
+ ///
+ /// One or more validation errors.
+ /// The value for .
+ /// The value for .
+ /// The value for . Defaults to "One or more validation errors occurred."
+ /// The value for .
+ /// The value for .
+ /// The created for the response.
+#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
+ public static ValidationProblem ValidationProblem(
+#pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
+ IEnumerable> errors,
string? detail = null,
string? instance = null,
string? title = null,
string? type = null,
- IDictionary? extensions = null)
+ IEnumerable>? extensions = null)
{
ArgumentNullException.ThrowIfNull(errors);
@@ -833,6 +868,17 @@ public static ValidationProblem ValidationProblem(
return new(problemDetails);
}
+ private static void CopyExtensions(IEnumerable>? extensions, ProblemDetails problemDetails)
+ {
+ if (extensions is not null)
+ {
+ foreach (var extension in extensions)
+ {
+ problemDetails.Extensions.Add(extension);
+ }
+ }
+ }
+
///
/// Produces a response.
///
diff --git a/src/Http/Http.Results/test/ResultsTests.cs b/src/Http/Http.Results/test/ResultsTests.cs
index 88513185c063..3e7220e717d0 100644
--- a/src/Http/Http.Results/test/ResultsTests.cs
+++ b/src/Http/Http.Results/test/ResultsTests.cs
@@ -1268,6 +1268,54 @@ public void Problem_WithArgs_ResultHasCorrectValues()
Assert.Equal(extensions, result.ProblemDetails.Extensions);
}
+ [Fact]
+ public void Problem_ResultHasCorrectValues()
+ {
+ // Arrange
+ var detail = "test detail";
+ var instance = "test instance";
+ var statusCode = StatusCodes.Status409Conflict;
+ var title = "test title";
+ var type = "test type";
+ var extensions = new List> { new("test", "value") };
+
+ // Act
+ var result = Results.Problem(detail, instance, statusCode, title, type, extensions) as ProblemHttpResult;
+
+ // Assert
+ Assert.Equal(detail, result.ProblemDetails.Detail);
+ Assert.Equal(instance, result.ProblemDetails.Instance);
+ Assert.Equal("application/problem+json", result.ContentType);
+ Assert.Equal(statusCode, result.StatusCode);
+ Assert.Equal(title, result.ProblemDetails.Title);
+ Assert.Equal(type, result.ProblemDetails.Type);
+ Assert.Equal(extensions, result.ProblemDetails.Extensions);
+ }
+
+ [Fact]
+ public void Problem_WithReadOnlyDictionary_ResultHasCorrectValues()
+ {
+ // Arrange
+ var detail = "test detail";
+ var instance = "test instance";
+ var statusCode = StatusCodes.Status409Conflict;
+ var title = "test title";
+ var type = "test type";
+ var extensions = (IReadOnlyDictionary)new Dictionary { ["test"] = "value" };
+
+ // Act
+ var result = Results.Problem(detail, instance, statusCode, title, type, extensions) as ProblemHttpResult;
+
+ // Assert
+ Assert.Equal(detail, result.ProblemDetails.Detail);
+ Assert.Equal(instance, result.ProblemDetails.Instance);
+ Assert.Equal("application/problem+json", result.ContentType);
+ Assert.Equal(statusCode, result.StatusCode);
+ Assert.Equal(title, result.ProblemDetails.Title);
+ Assert.Equal(type, result.ProblemDetails.Type);
+ Assert.Equal(extensions, result.ProblemDetails.Extensions);
+ }
+
[Theory]
[InlineData(StatusCodes.Status400BadRequest, "Bad Request", "https://tools.ietf.org/html/rfc9110#section-15.5.1")]
[InlineData(StatusCodes.Status418ImATeapot, "I'm a teapot", null)]
@@ -1375,6 +1423,37 @@ public void ValidationProblem_WithValidationProblemArg_ResultHasCorrectValues()
Assert.Equal(extensions, result.ProblemDetails.Extensions);
}
+ [Fact]
+ public void ValidationProblem_ResultHasCorrectValues()
+ {
+ // Arrange
+ var errors = new List> { new("testField", new[] { "test error" }) };
+ var detail = "test detail";
+ var instance = "test instance";
+ var statusCode = StatusCodes.Status412PreconditionFailed; // obscure for the test on purpose
+ var title = "test title";
+ var type = "test type";
+ var extensions = new List> { new("testField", "test value") };
+
+ // Act
+ // Note: Results.ValidationProblem returns ProblemHttpResult instead of ValidationProblem by design as
+ // as ValidationProblem doesn't allow setting a custom status code so that it can accurately report
+ // a single status code in endpoint metadata via its implementation of IEndpointMetadataProvider
+ var result = Results.ValidationProblem(errors, detail, instance, statusCode, title, type, extensions) as ProblemHttpResult;
+
+ // Assert
+ Assert.IsType(result.ProblemDetails);
+ Assert.Equal(errors, ((HttpValidationProblemDetails)result.ProblemDetails).Errors);
+ Assert.Equal(detail, result.ProblemDetails.Detail);
+ Assert.Equal(instance, result.ProblemDetails.Instance);
+ Assert.Equal(statusCode, result.ProblemDetails.Status);
+ Assert.Equal(statusCode, result.StatusCode);
+ Assert.Equal(title, result.ProblemDetails.Title);
+ Assert.Equal(type, result.ProblemDetails.Type);
+ Assert.Equal("application/problem+json", result.ContentType);
+ Assert.Equal(extensions, result.ProblemDetails.Extensions);
+ }
+
[Fact]
public void Redirect_WithNullStringUrl_ThrowsArgException()
{
diff --git a/src/Http/Http.Results/test/TypedResultsTests.cs b/src/Http/Http.Results/test/TypedResultsTests.cs
index 8e9a5c48d47a..acee20c4d15c 100644
--- a/src/Http/Http.Results/test/TypedResultsTests.cs
+++ b/src/Http/Http.Results/test/TypedResultsTests.cs
@@ -1084,6 +1084,30 @@ public void Problem_WithArgs_ResultHasCorrectValues()
Assert.Equal(extensions, result.ProblemDetails.Extensions);
}
+ [Fact]
+ public void Problem_ResultHasCorrectValues()
+ {
+ // Arrange
+ var detail = "test detail";
+ var instance = "test instance";
+ var statusCode = StatusCodes.Status409Conflict;
+ var title = "test title";
+ var type = "test type";
+ var extensions = new List> { new("test", "value") };
+
+ // Act
+ var result = TypedResults.Problem(detail, instance, statusCode, title, type, extensions);
+
+ // Assert
+ Assert.Equal(detail, result.ProblemDetails.Detail);
+ Assert.Equal(instance, result.ProblemDetails.Instance);
+ Assert.Equal("application/problem+json", result.ContentType);
+ Assert.Equal(statusCode, result.StatusCode);
+ Assert.Equal(title, result.ProblemDetails.Title);
+ Assert.Equal(type, result.ProblemDetails.Type);
+ Assert.Equal(extensions, result.ProblemDetails.Extensions);
+ }
+
[Theory]
[InlineData(StatusCodes.Status400BadRequest, "Bad Request", "https://tools.ietf.org/html/rfc9110#section-15.5.1")]
[InlineData(StatusCodes.Status418ImATeapot, "I'm a teapot", null)]
@@ -1186,6 +1210,32 @@ public void ValidationProblem_WithValidationProblemArg_ResultHasCorrectValues()
Assert.Equal(extensions, result.ProblemDetails.Extensions);
}
+ [Fact]
+ public void ValidationProblem_ResultHasCorrectValues()
+ {
+ // Arrange
+ var errors = new List> { new("testField", new[] { "test error" }) };
+ var detail = "test detail";
+ var instance = "test instance";
+ var title = "test title";
+ var type = "test type";
+ var extensions = new List> { new("testField", "test value") };
+
+ // Act
+ var result = TypedResults.ValidationProblem(errors, detail, instance, title, type, extensions);
+
+ // Assert
+ Assert.Equal(errors, result.ProblemDetails.Errors);
+ Assert.Equal(detail, result.ProblemDetails.Detail);
+ Assert.Equal(instance, result.ProblemDetails.Instance);
+ Assert.Equal(StatusCodes.Status400BadRequest, result.ProblemDetails.Status);
+ Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
+ Assert.Equal(title, result.ProblemDetails.Title);
+ Assert.Equal(type, result.ProblemDetails.Type);
+ Assert.Equal("application/problem+json", result.ContentType);
+ Assert.Equal(extensions, result.ProblemDetails.Extensions);
+ }
+
[Fact]
public void Redirect_WithNullStringUrl_ThrowsArgException()
{