Skip to content

Support all error's status codes ProblemDetail types for generating swagger docs in MinimalApi #47705

Closed
@mehdihadeli

Description

@mehdihadeli

Background and Motivation

If we want to create an api with multiple result types with using TypedResults, .net core 7 only supports ValidationProblem for auto generating swagger metadata and ProblemHttpResult type doesn't generate any swagger metadata (because it doesn't implement IEndpointMetadataProvider). Maybe we need to declare dedicated problem types like ValidationProblem for others error status codes, like InternalProbelm, UnAuthorizedProblem, NotFoundProblem...

There is an issue #47623 already for this proposal.

Proposed API

This is an example for UnAuthorized problem, but it is the same for other error status codes:

namespace Microsoft.AspNetCore.Http.HttpResults;

+ public sealed class UnAuthorizedHttpProblemResult
+       : IResult,
+       IEndpointMetadataProvider,
+        IStatusCodeHttpResult,
+        IContentTypeHttpResult,
+        IValueHttpResult
+{
+    internal UnAuthorizedHttpProblemResult(ProblemDetails problemDetails)
+    {
+        ArgumentNullException.ThrowIfNull(problemDetails);
+        if (problemDetails is { Status: not null and not StatusCodes.Status401Unauthorized })
+        {
+            throw new ArgumentException(
+                $"{nameof(UnAuthorizedHttpProblemResult)} only supports a 401 UnAuthorize response status code.",
+               nameof(problemDetails)
+            );
+       }
+
+        ProblemDetails = problemDetails;
+        ProblemDetailsDefaults.Apply(ProblemDetails, statusCode: StatusCodes.Status401Unauthorized);
+    }
+
+   public ProblemDetails ProblemDetails { get; }
+
+   object? IValueHttpResult.Value => ProblemDetails;
+
+   public string ContentType => "application/problem+json";
+
+   public int StatusCode => StatusCodes.Status401Unauthorized;
+
+    int? IStatusCodeHttpResult.StatusCode => StatusCode;
+
+
+    public Task ExecuteAsync(HttpContext httpContext)
+    {
+        ArgumentNullException.ThrowIfNull(httpContext);
+
+        var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
+        var logger = loggerFactory.CreateLogger(typeof(UnAuthorizedHttpProblemResult));
+
+       HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
+        httpContext.Response.StatusCode = StatusCode;
+
+        return HttpResultsHelper.WriteResultAsJsonAsync(httpContext, logger, value: ProblemDetails, ContentType);
+    }
+
+    static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)
+    {
+        ArgumentNullException.ThrowIfNull(method);
+        ArgumentNullException.ThrowIfNull(builder);
+
+        builder.Metadata.Add(
+            new ProducesResponseTypeMetadata(
+                typeof(ProblemDetails),
+                StatusCodes.Status401Unauthorized,
+                "application/problem+json"
+           )
+       );
+    }
+}
namespace Microsoft.AspNetCore.Http;

/// <summary>
/// A typed factory for <see cref="IResult"/> types in <see cref="Microsoft.AspNetCore.Http.HttpResults"/>.
/// </summary>
public static class TypedResults
{
+    public static UnAuthorizedHttpProblemResult UnAuthorizedProblem(
+      string? title = null,
+      string? detail = null,
+      string? instance = null,
+      string? type = null,
+      IDictionary<string, object?>? extensions = null
+   )
+   {
+        var problemDetails = CreateProblem(title, detail, instance, type, extensions);
+
+      return new UnAuthorizedHttpProblemResult(problemDetails);
+    }

+    private static ProblemDetails CreateProblem(
+        string? title,
+        string? detail,
+        string? instance,
+        string? type,
+        IDictionary<string, object?>? extensions
+    )
+    {
+        var problemDetails = new ProblemDetails
+        {
+           Detail = detail,
+            Instance = instance,
+           Type = type
+       };
+
+        problemDetails.Title = title ?? problemDetails.Title;
+
+        if (extensions is not null)
+        {
+            foreach (var extension in extensions)
+            {
+                problemDetails.Extensions.Add(extension);
+            }
+        }
+
+        return problemDetails;
+    }
}

Usage Examples

endpoints.MapPost("/products",
    Results<UnAuthorizedHttpProblemResult, Ok<ProductDto>>(ProductDto product) =>
    {
        if ( some condition )
           return TypedResults.UnauthorizedProblem();
         
           return TypedResults.Ok(product);
    });

Alternative Designs

N/A

Risks

None that I am aware of.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etc

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions