Skip to content

Should Minimal API's return ProblemDetails by default instead of an empty response body for error types? #60394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
1 task done
sander1095 opened this issue Feb 14, 2025 · 5 comments
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi
Milestone

Comments

@sander1095
Copy link
Contributor

sander1095 commented Feb 14, 2025

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

I am trying to replace usage of Controllers with Minimal API. My goal is to create the same API with the same error handling and OpenAPI metadata as I would with Controllers.

However, Minimal API's don't behave the same way as controllers do when it comes to returning ProblemDetails (and enriching the corresponding OpenAPI response content type) when errors like 400, 404, 500, etc.. are returned.

Minimal API's return an empty response body by default when using (Typed)Results.NotFound/BadRequest/Etc.., whereas controllers will automatically return a ProblemDetails instance with the corresponding error code (NotFound(),BadRequest(), etc..).
Controllers will even enrich the OpenAPI document with ProblemDetails when you specify [ProducesResponseType(StatusCodes.Status404NotFound)]; you don't even need to set the ProblemDetails type!

This means that you need to write a lot more code in a Minimal API to achieve consistent API error reporting. I'd like this be to be fixed by changing the way Minimal API's generate errors so they include ProblemDetails instances by default for each error.

Let's take a look at a reproducable example for controllers and minimal API. This is done with ASP.NET Core 9 and Microsoft.AspNetCore.OpenApi version 9.0.1. I've removed unnecessary parts of the OpenAPI documents to keep the issue easier to read.

Click here to see the Controllers approach

Code

[HttpGet("{id:int:min(1)}", Name = "Talks_GetTalk")]
[ProducesResponseType<TalkModel>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<CreateTalkModel> GetTalks(int id)
{
    var talk = SampleTalks.Talks.FirstOrDefault(x => x.Id == id);
    if (talk == null)
    {
        return NotFound();
    }
    return Ok(talk);
}

OpenAPI document

{
  "paths": {
    "/api/talks/{id}": {
      "get": {
        "tags": [
          "Talks"
        ],
        "operationId": "Talks_GetTalk",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/TalkModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TalkModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/TalkModel"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    }
  }
}

As you can see, the OpenAPI document specifies the ProblemDetails response body for the 404 not found error, without any extra code required in the controller. I consider this a great default!

Click here to see the Minimal API approach

Code

var api = app.MapGroup("api/talks");
api.MapGet("/{id:int:min(1)}", GetTalk).WithName("Talks_GetTalk");

public static Results<Ok<TalkModel>, NotFound> GetTalk(int id)
{
    var talk = SampleTalks.Talks.FirstOrDefault(x => x.Id == id);
    return talk == null ?
        TypedResults.NotFound() :
        TypedResults.Ok(talk);
}

OpenAPI document

{
  "paths": {
    "/api/talks/{id}": {
      "get": {
        "tags": [
          "TalkEndpoints"
        ],
        "operationId": "Talks_GetTalk",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "minimum": 1,
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TalkModel"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        }
      }
    }
  }
}

Creating feature-parity between Controllers and Minimal API

As you can see above, the minimal API does not contain ProblemDetails with its out-of-the-box approach.

In order to get ProblemDetails to work for the NotFound error, I would need to write the following code:

// The ProblemHttpResult is not present in the OpenAPI document, as explained in https://github.com/dotnet/aspnetcore/issues/58719
// To ensure that it is added, we can use a combination of TypedResults and extension methods (which is not ideal).
// We could also fall back to using Results and use extension methods for the OpenAPI metadata, but we'd lose compile-time safety..
api.MapGet("/{id:int:min(1)}", GetTalk)
    .WithName("Talks_GetTalk")
    .ProducesProblem(StatusCodes.Status404NotFound);

public static Results<Ok<TalkModel>, ProblemHttpResult> GetTalk(int id)
{
    // This still excludes the traceid from the ProblemDetails instance for brevity purposes
    var talk = SampleTalks.Talks.FirstOrDefault(x => x.Id == id);
    return talk == null ?
        TypedResults.Problem(title: "Not Found", statusCode: StatusCodes.Status404NotFound) :
        TypedResults.Ok(talk);
}

This will result in the OpenAPI document containing these responses for the operation:

"responses": {
  "200": {
    "description": "OK",
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/TalkModel"
        }
      }
    }
  },
  "404": {
    "description": "Not Found",
    "content": {
      "application/problem+json": {
        "schema": {
          "$ref": "#/components/schemas/ProblemDetails"
        }
      }
    }
  }
}

This is not 100% equal to the controllers, but close enough to get the same API behavior.

Alternatively, perhaps I could write some middleware (and/or adjust the way the OpenAPI document is generated) to get these things to work, instead of having to modify each endpoint. But I believe there would be downsides around code clarity and performance.

To summarize, my problem with the Minimal API approach is that it takes too much custom code to get the same consistent API error behaviour that controllers give you. I understand there's a delicate balance between Minimal API's and controllers; Minimal API's are minimal for a reason. However, I hope the team and community can understand and agree with the need for a default, consistent API surface when creating ASP.NET Core minimal apps when it comes to errors.

Describe the solution you'd like

The out-of-the-box behaviour from controllers works well to create a consistent API surface. Because every error (for existing endpoints) will be turned into a ProblemDetails (by default), handwritten or generated API clients will always be able to deserialize into ProblemDetails, making it easier for clients to perform error handling.

I'd like the same behaviour in Minimal API's.

This would also matter for the (AFAIK) upcoming request validation in Minimal API's. I can't find a related issue, so I'll assume it works the same as with controllers. where I'd like the OpenAPI document to contain a ValidationProblemDetails without me having to manually perform validation, set up a (validation) problem details instance, and decorate the endpoint with this new return value.

So, in summary, I think allowing the following code to generate the following OpenAPI document would be a great productivity boost, reduce code clutter and create more consistent API surfaces (when calling endpoints that exists).

An example:

Code

var api = app.MapGroup("api/talks");
api.MapGet("/{id:int:min(1)}", GetTalk).WithName("Talks_GetTalk");

public static Results<Ok<TalkModel>, NotFound> GetTalk(int id)
{
    var talk = SampleTalks.Talks.FirstOrDefault(x => x.Id == id);
    return talk == null ?
        TypedResults.NotFound() :
        TypedResults.Ok(talk);
}

OpenAPI document

"responses": {
  "200": {
    "description": "OK",
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/TalkModel"
        }
      }
    }
  },
  "404": {
    "description": "Not Found",
    "content": {
      "application/problem+json": {
        "schema": {
          "$ref": "#/components/schemas/ProblemDetails"
        }
      }
    }
  }
}

Additional context

Related issues

I've found an issue that is a little bit related: #59560 , which in turn might be related to #58719 . One of the comments from @mikekistler says:

I think this approach may not work well with the generation of OpenAPI documents that was added in .NET 9. In particular, I think it would require ProducesResponseType attributes to be specified in order to get the error responses to be included in the generated OpenAPI document. This is because TypedResults.Problem does not provide metadata for the OpenAPI document, unlike other TypedResults methods like TypedResults.BadRequest. Is that important to you?

This is important to me. If I currently want a consistent API surface where each error returns ProblemDetails, I would need to use TypedResults.Problem. However, if this doesn't add metadata about this response to the the OpenAPI document, setting up OpenAPI metadata becomes more painful for Minimal API compared to the current approach with controllers which is explained above.

Further context

The discussion started in #60337 . There's not a lot of context there, but I thought it'd be worth adding.

@martincostello martincostello added feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed area-web-frameworks labels Feb 14, 2025
@mikekistler mikekistler added this to the Backlog milestone Feb 28, 2025
@mikekistler
Copy link
Contributor

Thanks for filing this issue!

I thought the answer was to just add the ProblemDetails service to builder.Services, and then app.UseStatusCodePages (very non-intuitive name there). This does generate problem details responses for non-generic TypedResults like TypedResults.NotFound(). But the generated OpenAPI does not describe the response content.

@captainsafia says: Because the ProblemDetails is serialized by the ProblemDetails service, they don't show up in the ProducesResponseTypeMetadata as is the case with generic TypedResults that return actual types. As a result of this, ApiExplorer/OpenAPI don't pick up the ProblemDetails as an actual response type returned by the API and we don't end up generating a schema for it.
 
We need a way to allow the ProblemDetails service to influence what response types are actually set in metadata by non-generic TypedResults. That would solve our problem.

I've marked this as a Feature and put it in our backlog. We'll look for an opportunity to squeeze this into .NET 10.

@sander1095
Copy link
Contributor Author

Hey @mikekistler! That all sounds great. What are your thoughts on improving the do to make these odd requirements of the problemdetailsservice and statuscodepages easier to find?

On top of that, perhaps there should be some docs on Controller -> Minimal API migrations and their behavioral differences, which is the case here, and how to get these differences restored?

Alternatively, we could change behavior to get ProblemDetails to be returned by default, but I'm guessing that ship has sailed because we don't want breaking changes?

@mikekistler
Copy link
Contributor

We are hoping to improve the APIs section of the docs over the .NET 10 timeframe. I think the first step is to make the TOC for controller-based and minimal more consistent. We may also break out some topics, like we did with OpenAPI, to be peers of controller-based and minimal APIs and then call out differences between them within the topic, using tabs.

Alternatively, we could change behavior to get ProblemDetails to be returned by default,

Maybe, but only if we are sure it won't be considered a breaking change.

@sander1095
Copy link
Contributor Author

Maybe, but only if we are sure it won't be considered a breaking change.

Is the team interested in exploring this further for .NET 10/11? If so, I'd love to help, so let me know what I can do. Perhaps we can chat about it at the MVP Summit :).

@mikekistler
Copy link
Contributor

@sander1095 We can explore this and hopefully can discuss IRL. Something else to consider is that Controllers and Minimal, while having mostly the same capabilities, seem to have different philosophies. Controllers seem to build things in by default, requiring explicit action to turn them off, whereas Minimal (as its name implies) makes many features "opt-in". We should be careful not to "cross the streams" here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi
Projects
None yet
Development

No branches or pull requests

3 participants