Skip to content
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

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

Closed
IanBuck-dev opened this issue Oct 28, 2021 · 7 comments · Fixed by #40088
Closed

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

IanBuck-dev opened this issue Oct 28, 2021 · 7 comments · Fixed by #40088
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-minimal-actions Controller-like actions for endpoint routing feature-openapi old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels Priority:0 Work that we can't release without
Milestone

Comments

@IanBuck-dev
Copy link

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

@mkArtakMSFT mkArtakMSFT added the old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Oct 28, 2021
@rafikiassumani-msft
Copy link
Contributor

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

@nilzzzzzz
Copy link

@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
Copy link
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
Copy link

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

@rafikiassumani-msft rafikiassumani-msft added the feature-minimal-actions Controller-like actions for endpoint routing label Dec 27, 2021
@captainsafia captainsafia self-assigned this Jan 24, 2022
@rafikiassumani-msft rafikiassumani-msft added Cost:M Priority:0 Work that we can't release without labels Jan 25, 2022
@rafikiassumani-msft rafikiassumani-msft added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Feb 3, 2022
@ghost
Copy link

ghost commented Feb 3, 2022

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.

@captainsafia
Copy link
Member

captainsafia commented Feb 7, 2022

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
Copy link
Member

captainsafia commented Feb 9, 2022

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.""");

@captainsafia captainsafia added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels Feb 15, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Mar 24, 2022
@amcasey amcasey added the area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc label Jun 2, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-minimal-actions Controller-like actions for endpoint routing feature-openapi old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels Priority:0 Work that we can't release without
Projects
None yet
7 participants