Skip to content

Unable to set route description or summary with minimal api #37906

Closed
@IanBuck-dev

Description

@IanBuck-dev

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

Screenshot 2021-10-28 at 17 00 56

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

Activity

rafikiassumani-msft

rafikiassumani-msft commented on Oct 28, 2021

@rafikiassumani-msft
Contributor

Triage: We are planning to fix this as part of .NET7 issue #37098

nilzzzzzz

nilzzzzzz commented on Oct 28, 2021

@nilzzzzzz

@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.

martincostello

martincostello commented on Oct 29, 2021

@martincostello
Member

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

nilzzzzzz

nilzzzzzz commented on Oct 29, 2021

@nilzzzzzz

@martincostello bam! thank you! I already thought we need to move back to mvc controllers. ;)

self-assigned this
on Jan 24, 2022
ghost
captainsafia

captainsafia commented on Feb 7, 2022

@captainsafia
Member

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 value that represents an inline implementation of the type or an externalValue which is an external reference to an example.
    • value and externalValue are mutually exclusive.

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 WithRequestExample and only allowing the RequestBody to 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 DescriptionAttribute and opt for respecting System.ComponentModel.DescriptionAttribute instead?
  • Should we define a WithParameterExample extension method for defining examples for parameters with types that can't be initialized as constants in an attribute?
  • Thoughts on the WithParameterX, WithResponseX pattern?
captainsafia

captainsafia commented on Feb 9, 2022

@captainsafia
Member

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 IExampleMetadata outlined 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.""");
added
api-approvedAPI was approved in API review, it can be implemented
and removed
api-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviews
on Feb 15, 2022
ghost locked as resolved and limited conversation to collaborators on Mar 24, 2022
added
area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etc
on Jun 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Priority:0Work that we can't release withoutapi-approvedAPI was approved in API review, it can be implementedarea-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-minimal-actionsController-like actions for endpoint routingfeature-openapiold-area-web-frameworks-do-not-use*DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels

Type

No type

Projects

No projects

Relationships

None yet

    Participants

    @martincostello@captainsafia@amcasey@nilzzzzzz@mkArtakMSFT

    Issue actions

      Unable to set route description or summary with minimal api · Issue #37906 · dotnet/aspnetcore