From 007c622fdddc71239fad4c30cf6f86c7c29fa83f Mon Sep 17 00:00:00 2001
From: Safia Abdalla <safia@microsoft.com>
Date: Thu, 23 Sep 2021 14:00:41 -0700
Subject: [PATCH] Improve Results.Problem and Results.ValidationProblem APIs
 (#36856)

---
 .../Http.Results/src/PublicAPI.Unshipped.txt  |  7 +--
 src/Http/Http.Results/src/Results.cs          | 44 ++++++++++++++++---
 .../MinimalSample/MinimalSample.csproj        |  2 +
 src/Http/samples/MinimalSample/Program.cs     | 27 +++++++++---
 4 files changed, 66 insertions(+), 14 deletions(-)

diff --git a/src/Http/Http.Results/src/PublicAPI.Unshipped.txt b/src/Http/Http.Results/src/PublicAPI.Unshipped.txt
index 96ce109da59..7eff16008f1 100644
--- a/src/Http/Http.Results/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Results/src/PublicAPI.Unshipped.txt
@@ -20,7 +20,8 @@ static Microsoft.AspNetCore.Http.Results.LocalRedirect(string! localUrl, bool pe
 static Microsoft.AspNetCore.Http.Results.NoContent() -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.Results.NotFound(object? value = null) -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.Results.Ok(object? value = null) -> 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) -> 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.IDictionary<string!, object?>? extensions = null) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Problem(Microsoft.AspNetCore.Mvc.ProblemDetails! problemDetails) -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.Results.Redirect(string! url, bool permanent = false, bool preserveMethod = false) -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.Results.RedirectToRoute(string? routeName = null, object? routeValues = null, bool permanent = false, bool preserveMethod = false, string? fragment = null) -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.Results.SignIn(System.Security.Claims.ClaimsPrincipal! principal, Microsoft.AspNetCore.Authentication.AuthenticationProperties? properties = null, string? authenticationScheme = null) -> Microsoft.AspNetCore.Http.IResult!
@@ -30,6 +31,6 @@ static Microsoft.AspNetCore.Http.Results.Stream(System.IO.Stream! stream, string
 static Microsoft.AspNetCore.Http.Results.Text(string! content, string? contentType = null, System.Text.Encoding? contentEncoding = null) -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.Results.Unauthorized() -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.Results.UnprocessableEntity(object? error = null) -> Microsoft.AspNetCore.Http.IResult!
-static Microsoft.AspNetCore.Http.Results.ValidationProblem(System.Collections.Generic.IDictionary<string!, string![]!>! errors, string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.ValidationProblem(System.Collections.Generic.IDictionary<string!, string![]!>! errors, string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null, System.Collections.Generic.IDictionary<string!, object?>? extensions = null) -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.Results.Extensions.get -> Microsoft.AspNetCore.Http.IResultExtensions!
-Microsoft.AspNetCore.Http.IResultExtensions
\ No newline at end of file
+Microsoft.AspNetCore.Http.IResultExtensions
diff --git a/src/Http/Http.Results/src/Results.cs b/src/Http/Http.Results/src/Results.cs
index 15d4937e3a2..aa69b20ea4d 100644
--- a/src/Http/Http.Results/src/Results.cs
+++ b/src/Http/Http.Results/src/Results.cs
@@ -471,13 +471,15 @@ namespace Microsoft.AspNetCore.Http
         /// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
         /// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
         /// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
+        /// <param name="extensions">The value for <see cref="ProblemDetails.Extensions" />.</param>
         /// <returns>The created <see cref="IResult"/> for the response.</returns>
         public static IResult Problem(
             string? detail = null,
             string? instance = null,
             int? statusCode = null,
             string? title = null,
-            string? type = null)
+            string? type = null,
+            IDictionary<string, object?>? extensions = null)
         {
             var problemDetails = new ProblemDetails
             {
@@ -485,9 +487,30 @@ namespace Microsoft.AspNetCore.Http
                 Instance = instance,
                 Status = statusCode,
                 Title = title,
-                Type = type
+                Type = type,
             };
 
+            if (extensions is not null)
+            {
+                foreach (var extension in extensions)
+                {
+                    problemDetails.Extensions.Add(extension);
+                }
+            }
+
+            return new ObjectResult(problemDetails)
+            {
+                ContentType = "application/problem+json",
+            };
+        }
+
+        /// <summary>
+        /// Produces a <see cref="ProblemDetails"/> response.
+        /// </summary>
+        /// <param name="problemDetails">The <see cref="ProblemDetails"/>  object to produce a response from.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Problem(ProblemDetails problemDetails)
+        {
             return new ObjectResult(problemDetails)
             {
                 ContentType = "application/problem+json",
@@ -502,8 +525,9 @@ namespace Microsoft.AspNetCore.Http
         /// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
         /// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
         /// <param name="statusCode">The status code.</param>
-        /// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
+        /// <param name="title">The value for <see cref="ProblemDetails.Title" />. Defaults to "One or more validation errors occurred."</param>
         /// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
+        /// <param name="extensions">The value for <see cref="ProblemDetails.Extensions" />.</param>
         /// <returns>The created <see cref="IResult"/> for the response.</returns>
         public static IResult ValidationProblem(
             IDictionary<string, string[]> errors,
@@ -511,16 +535,26 @@ namespace Microsoft.AspNetCore.Http
             string? instance = null,
             int? statusCode = null,
             string? title = null,
-            string? type = null)
+            string? type = null,
+            IDictionary<string, object?>? extensions = null)
         {
             var problemDetails = new HttpValidationProblemDetails(errors)
             {
                 Detail = detail,
                 Instance = instance,
-                Title = title,
                 Type = type,
                 Status = statusCode,
             };
+            
+            problemDetails.Title = title ?? problemDetails.Title;
+
+            if (extensions is not null)
+            {
+                foreach (var extension in extensions)
+                {
+                    problemDetails.Extensions.Add(extension);
+                }
+            }
 
             return new ObjectResult(problemDetails)
             {
diff --git a/src/Http/samples/MinimalSample/MinimalSample.csproj b/src/Http/samples/MinimalSample/MinimalSample.csproj
index 6b59d1446b9..eea90b96520 100644
--- a/src/Http/samples/MinimalSample/MinimalSample.csproj
+++ b/src/Http/samples/MinimalSample/MinimalSample.csproj
@@ -8,6 +8,8 @@
     <Reference Include="Microsoft.AspNetCore" />
     <Reference Include="Microsoft.AspNetCore.Diagnostics" />
     <Reference Include="Microsoft.AspNetCore.Hosting" />
+    <Reference Include="Microsoft.AspNetCore.Http" />
+    <Reference Include="Microsoft.AspNetCore.Http.Results" />
     <!-- Mvc.Core is referenced only for its attributes -->
     <Reference Include="Microsoft.AspNetCore.Mvc.Core" />
     <Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
diff --git a/src/Http/samples/MinimalSample/Program.cs b/src/Http/samples/MinimalSample/Program.cs
index 5441e671a8f..12b78a78246 100644
--- a/src/Http/samples/MinimalSample/Program.cs
+++ b/src/Http/samples/MinimalSample/Program.cs
@@ -1,9 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.Extensions.Hosting;
+using Microsoft.AspNetCore.Mvc;
 
 var app = WebApplication.Create(args);
 
@@ -13,12 +11,29 @@ if (app.Environment.IsDevelopment())
 }
 
 string Plaintext() => "Hello, World!";
-app.MapGet("/plaintext", (Func<string>)Plaintext);
+app.MapGet("/plaintext", Plaintext);
+
 
 object Json() => new { message = "Hello, World!" };
-app.MapGet("/json", (Func<object>)Json);
+app.MapGet("/json", Json);
 
 string SayHello(string name) => $"Hello, {name}!";
-app.MapGet("/hello/{name}", (Func<string, string>)SayHello);
+app.MapGet("/hello/{name}", SayHello);
+
+var extensions = new Dictionary<string, object>() { { "traceId", "traceId123" } };
+
+app.MapGet("/problem", () =>
+    Results.Problem(statusCode: 500, extensions: extensions));
+
+app.MapGet("/problem-object", () =>
+    Results.Problem(new ProblemDetails() { Status = 500, Extensions = { { "traceId", "traceId123"} } }));
+
+var errors = new Dictionary<string, string[]>();
+
+app.MapGet("/validation-problem", () =>
+    Results.ValidationProblem(errors, statusCode: 400, extensions: extensions));
+
+app.MapGet("/validation-problem-object", () =>
+    Results.Problem(new HttpValidationProblemDetails(errors) { Status = 400, Extensions = { { "traceId", "traceId123"}}}));
 
 app.Run();
-- 
GitLab