Unable to set route description or summary with minimal api
IanBuck-dev opened this issue · 7 comments
Is your feature request related to a problem? Please describe.
Hi, I am using the new minimal hosting model with dotnet 6 and I set up my endpoints like this:
endpoints.MapPost("user/createWithList", ExecutionDelegate).WithTags("user");Is there a way to set the summary of the route, so right next to the route -> underlined with red?
Is there a way to set the extended description of the route, so below the http method, in this case POST, and above the parameters section -> marked by the red arrow?
I already checked and there seem to only be the WithTags method or the AddMetaData where you could add EndpointNameMetadata
I think this feature is essential for providing a well structured and helpful api documentation.
Describe the solution you'd like
I would like to have the option to add the description either by having a dedicated method for that, so:
endpoints.MapPost("user/createWithList", ExecutionDelegate).WithTags("user").WithDescription("This endpoints lets you create users for ...");or to have attributes like the EndpointNameMetadata f.e. EndpointDescriptionMetadata which can be used to set the OpenApi description of that route and be passed to the WithMetadata() method
Triage: We are planning to fix this as part of .NET7 issue #37098
@rafikiassumani-msft thanks for the update. Is there any entry point where we could implement the functionality to add an endpoint description ourself till the gap is potentially closed in .NET7 ? Any hint would be highly appreciated.
This should light up when using SwaggerOperationAttribute in Swashbuckle.AspNetCore for .NET 6 once this change is merged and released: domaindrivendev/Swashbuckle.AspNetCore#2215.
You can see some examples of working around it in this repo of mine: https://github.com/martincostello/api/search?q=SwaggerOperationAttribute
@martincostello bam! thank you! I already thought we need to move back to mvc controllers. ;)
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
- The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
- The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
- Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.
Background and Motivation
The OpenAPI schema provides support for annotating properties with summaries, descriptions, and examples.
- Summaries are short descriptions of the functionality of a path or operation (AKA an endpoint).
- Descriptions are longer, multi-line descriptions of an endpoint, parameter, etc.
- Examples can describe a parameter, request body, or response.
- Examples have either a
valuethat represents an inline implementation of the type or anexternalValuewhich is an external reference to an example. valueandexternalValueare mutually exclusive.
- Examples have either a
This PR introduces support for these annotations in minimal APIs via declarative and imperative approaches. It does not modify any of the logic in MVC though. That is supported by XML-doc parsing support in Swashbuckle which pulls the data above from XML docs on an action/endpoint. We are tracking #39927 to look at supporting this pattern for minimal APIs as well.
Proposed API
Description and Summary
namespace Microsoft.AspNetCore.Http.Metadata
{
public interface ISummaryMetadata
{
string Summary { get; }
}
public interface IDescriptionMetadata
{
string Description { get; }
}
}
namespace Microsoft.AspNetCore.Http
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)]
public sealed class SummaryAttribute : Attribute, ISummaryMetadata
{
public SummaryAttribute(string summary)
public string Summary { get; }
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
public sealed class DescriptionAttribute : Attribute, IDescriptionMetadata
{
public DescriptionAttribute(string description)
public string Description { get; }
}
}ExampleMetadata
namespace Microsoft.AspNetCore.Http.Metadata
{
public interface IExampleMetadata
{
string Summary { get; }
string Description { get; }
object? Value { get; }
string? ExternalValue { get; }
string? ParameterName { get; set; }
}
}
namespace Microsoft.AspNetCore.Http
{
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
public sealed class ExampleAttribute : Attribute, IExampleMetadata
{
public ExampleAttribute(string summary, string description, object value)
public ExampleAttribute(string summary, string description, string externalValue)
public string Description { get; }
public string Summary { get; }
public object? Value { get; }
public string? ExternalValue { get; }
public string? ParameterName { get; set; }
}
}Extension Methods
namespace Microsoft.AspNetCore.Http
{
public static class OpenApiRouteHandlerBuilderExtensions
{
public static RouteHandlerBuilder WithResponseExample(
this RouteHandlerBuilder builder,
string summary,
string description,
object value)
public static RouteHandlerBuilder WithResponseExample(
this RouteHandlerBuilder builder,
string summary,
string description,
string externalValue)
public static RouteHandlerBuilder WithParameterExample(
this RouteHandlerBuilder builder,
string summary,
string description,
object value)
public static RouteHandlerBuilder WithParameterExample(
this RouteHandlerBuilder builder,
string summary,
string description,
string externalValue)
public static RouteHandlerBuilder WithDescription(this RouteHandlerBuilder builder, string description)
public static RouteHandlerBuilder WithSummary(this RouteHandlerBuilder builder, string summary)
}ApiExplorer Changes
namespace Microsoft.AspNetCore.Mvc.ApiExplorer;
public class ApiResponseType
{
public IEnumerable<IExampleMetadata>? Examples { get; set; }
public string? Description { get; set; }
}
public class ApiParameterDescription
{
public IEnumerable<IExampleMetadata>? Examples { get; set; }
public string? Description { get; set; }
}Usage Examples
var app = WebApplication.Create(args);
// Add a summary to an endpoint
app
.MapGet("/todos/{id}", (int id, TodoService todos) => todos.Get(id))
.WithSummary("Resolves a Todo item from the backend service.")
// Add a description to an endpoint
app
.MapGet("/todos/{id}", (int id, TodoService todos) => todos.Get(id))
.WithDescription("""This endpoint returns a Todo given an ID.
If the request is unauthenticated then the backend API
will resolve a sample todo and issue a redirect to the
login page.""");
// Add a description to a parameter
app
.MapGet("/todos/{id}", ([Description("The ID of the todo...")]int id, TodoService todos) => todos.Get(id));
// Add an example to a parameter
app.MapGet("/todos/{id}",
([Example("An example for the provided parameter", "Some long description", 2)] int id, TodoService todos) => todos.Get(id));
// Add an example to a parameter via extension method
app
.MapGet("/todos/{id}", (int id, TodoService todos) => todos.Get(id))
.WithParameterExample("id", "An example for the provided parameter", "Some long description", 2);
// Add an example for the request body
app
.MapPost("/todos", (Todo todo, TodoService todos) => todos.CreateTodo(todo))
.WithParameterExample("Example of value todo", "When a todo is scheduled, the Todo...", new Todo() { Id = 1 })
.WithParameterExample("Example of externalValue todo", "When a todo is scheduled, the Todo...", "https://examples.com/foo/bar");
// Add an example for the response
app
.MapGet("/todos/{id}", (int id, TodoService todos) => todos.Get(id))
.WithResponseExample("This is a response with a value", "Some long description", new Todo() { Id = 1 })
.WithResponseExample("This is a response with an external value", "Some long description", "https://example.com/foo/bar");Alternative Designs and Considerations
- Removing the
WithRequestExampleand only allowing theRequestBodyto be annotated via an attribute.- Does not work well since attributes only permit
- Define
IExampleMetadata<T>to support a better experience for the developer.- Makes it difficult to discover metadata and attributes in the description provider at runtime without using reflection.
- Should we avoid redefining a new
DescriptionAttributeand opt for respectingSystem.ComponentModel.DescriptionAttributeinstead? - Should we define a
WithParameterExampleextension method for defining examples for parameters with types that can't be initialized as constants in an attribute? - Thoughts on the
WithParameterX,WithResponseXpattern?
We held a little API review for this and decided to go in a different direction with this implementation.
Supporting Description and Summary on endpoints is very sensible and fine, but the ahem scope creep ahem I introduced by exploring the requirements to add support for parameter and response-type specific properties introduces a couple of thorny questions:
- The API for
IExampleMetadataoutlined above aligns very closely with the OpenAPI spec and gets us into tricky territory with having to align our APIs with the evolution of the OpenAPI spec. - If we start with
IExampleMetadata, where do we end? - The prevalence of so many extension methods has the tendency to populate the endpoint builder with a lot of concepts that are specific to OpenAPI but not the endpoint.
With that in mind, we've opened #40084 to examine creating a unified extension method for endpoints that allows modifying all aspects of an API description.
And, we've limited the scope of this change to what was strictly in the original issue, supporting descriptions and summaries in endpoints. So now the new API we're taking through review is:
Interfaces and Classes
namespace Microsoft.AspNetCore.Http.Metadata
{
public interface ISummaryMetadata
{
string Summary { get; }
}
public interface IDescriptionMetadata
{
string Description { get; }
}
}
namespace Microsoft.AspNetCore.Http
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)]
public sealed class SummaryAttribute : Attribute, ISummaryMetadata
{
public SummaryAttribute(string summary)
public string Summary { get; }
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
public sealed class DescriptionAttribute : Attribute, IDescriptionMetadata
{
public DescriptionAttribute(string description)
public string Description { get; }
}
}Extension Methods
namespace Microsoft.AspNetCore.Http
{
public static class OpenApiRouteHandlerBuilderExtensions
{
public static RouteHandlerBuilder WithDescription(this RouteHandlerBuilder builder, string description)
public static RouteHandlerBuilder WithSummary(this RouteHandlerBuilder builder, string summary)
}
}Usage Examples
app
.MapGet("/todos/{id}", [Summary("A summary)] (int id, TodoService todos) => todos.Get(id));
app
.MapGet("/todos/{id}", [Description("A description)] (int id, TodoService todos) => todos.Get(id));
app
.MapGet("/todos/{id}", (int id, TodoService todos) => todos.Get(id))
.WithSummary("Resolves a Todo item from the backend service.")
// Add a description to an endpoint
app
.MapGet("/todos/{id}", (int id, TodoService todos) => todos.Get(id))
.WithDescription("""This endpoint returns a Todo given an ID.
If the request is unauthenticated then the backend API
will resolve a sample todo and issue a redirect to the
login page.""");