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() {