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

Support WithApiDescription extension method for minimal APIs #40084

Closed
captainsafia opened this issue Feb 9, 2022 · 5 comments
Closed

Support WithApiDescription extension method for minimal APIs #40084

captainsafia opened this issue Feb 9, 2022 · 5 comments
Assignees
Labels
enhancement This issue represents an ask for new feature or an enhancement to an existing one 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:1 Work that is critical for the release, but we could probably ship without
Milestone

Comments

@captainsafia
Copy link
Member

captainsafia commented Feb 9, 2022

Summary

Minimal APIs currently support annotating endpoints with metadata that can be used to generated OpenAPI descriptions for a particular endpoint. Currently, this endpoint can be used to annotate endpoints with details about the request parameters and responses and descriptive details (tags, summaries, descriptions).

However, the OpenAPI spec provides support for a larger variety of annotations including ones that describe the parameters of an endpoint specifically (like examples for a particular parameter). There's also a matter of the fact that the OpenAPI specification can introduce new annotations at any time and we have to ensure that we are compatible.

These circumstances present the requirement for an API that allows the user to more flexibly describe the API associated with an endpoint.

Goals

  • Allow users to annotate individual parameters and responses
  • Allow user to modify descriptions that are applied to the API by default
  • Allow us to detach a little bit from the ApiExplorer model used in MVC

Non-goals

  • Strictly match the OpenAPI specification with regard to what strongly-typed properties we expose

Proposed Design Walkthrough

Let's say that the user wants to annotate the following endpoint with these details:

  • Examples for the Todo that gets passed the body
  • Document that Todo parameter is not required
  • Examples for the response
  • Description for the id parameter
  • Description for the Todo parameter
  • Name and summary for the endpoint
app.MapPut(
  "/api/todos/{id}",
  (int id, [Description("This is a default")] Todo updatedTodo, TodosService todosService) => {
  	todosService.Update(id, updatedTodo);
	})
  .WithName("GetFoo")
  .WithDescription("This is an endpoint for updating a todo")

To make things a little bit more interesting, we'll also assume that the endpoint already contains annotations
using our currently supported patterns (extension methods and attributes) that we would like to override with our new strategy. This will help show how the two implementations can intersect.

The user will leverage a new extension method in the framework:

public static RouteHandlerBuilder WithApiDescription(this RouteHandlerBuilder builder, Action<EndpointApiDescription> configureDescription);

To annotate their API with the desired schema, the user will provide a function that takes an EndpointApiDescription, modifies the desired properties, and returns the modified EndpointApiDescription.

.WithApiDescription(schema => {
	schema.Parameters["updatedTodo"].Items["Examples"] = new Todo { ... };
	schema.Parameters["updatedTodo"].Description = "New description";
	schema.Parameters["id"]["Type"] = typeof(string);
	schema.Responses[StatusCodes.Status200OK].Items["Examples"] = new Todo { ... };
	schema.EndpointName = "UpdateTodo";
	schema.EndpointDescription = "A filter for updating a todo";
});

The EndpointApiDescription is a new class that represents the API description. It contains a mix of strongly-typed properties and an Items bag that can be used to add/override arbitrary fields.

public class EndpointApiDescription
{
	public string EndpointName;
	public string EndpointDescription;
	public string[] EndpointTags;
	public Dictionary<string, EndpointApiParameter> Parameters;
	public Dictionary<StatusCode, EndpointApiResponse> Responses;
	public Dictionary<string, object>? Items;
}

The EndpointApiDescription in turn references two new types: EndpointApiParameter and EndpointApiResponse that follow a similar model.

public class EndpointApiResponse
{
	public StatusCodes StatusCode { get; }
	public string Description { get; set; }
	public Dictionary<string, object> Items { get; set; }
}

public class EndpointApiParameter
{
	public string Name { get; }
	public Type ParameterType { get; }
	public string Description { get; set; }
	public Dictionary<string, object> Items { get; set; }
}

The WithApDescription will register the configureDescription delegate that is provided and store it in the metadata of the targeted endpoint.

This change will be coupled with some changes to the EndpointMetadataApiDescriptionProvider that will register the constructs produced by the provider onto the schema, call the users configureDescription method, and set the resulting EndpointApiDescription onto the metadata for consumption by external libraries.

This change will require that ecosystem libraries like Swashbuckle and NSwag respect the new EndpointApiDescription class and that some conventions be adopted around how certain properties are represented. For example, since the Examples property is not supported as a strongly-typed field, conventions will need to be established around storing it in description.Parameters["todo"].Items["Examples"].

Back to the WitApiDescription method, behind the scenes it registers the delegate provided to the user as part of the metadata associated with the endpoint.

public static RouteHandlerBuilder WithApiDescription(this RouteHandlerBuilder builder, Action<EndpointApiDescription> configureSchema)
{
	builder.WithMetadata(configureSchema);
}

The configureSchema method is called from the EndpointMetadataApiDescriptionProvider after the API description has been constructed. In order to support this scenario, we will need to refactor the provider so that the following logic can be invoked.

foreach (var httpMethod in httpMethodMetadata.HttpMethods)
{
  var schema = CreateDefaultApiSchema(routeEndpoint, httpMethod, ...);
  var withApiDescription = routeEndpoint.Metadata.GetMetadata<Action<EndpointApiDescription>>();
  if (withApiDescription is not null)
  {
      withApiDescription(schema);
  }
  var apiDescription = CreateApiDescriptionFromSchema(modifiedSchema);
  context.Results.Add(apiDescription);
}

The CreateApiDescriptionFromSchema maps the new EndpointApiDescription type to the existing ApiDescription type by using the following mappings:

  • We maintain the existing semantics for setting endpoint-related like GroupName and RelativePath
  • We maintain the existing semantics for setting things like parameter types and content-types associated with responses
  • EndpointApiDescription.Parameters and EndpointApiDescription.Responses get populated into ApiDescription.Properties

With this in mind, the previous flow for API generation looked like this:

flowchart LR
  A[Attributes]-->B[Metadata]
  C[Extension Methods]-->B[Metadata]
  B[Metadata]-->D[EndpointAPIDescriptionProvider]
  D[EndpointAPIDescriptionProvider]-->E[ApiDescription]
Loading

To the following:

flowchart LR
  A[Attributes]-->B[Metadata]
  C[Extension Methods]-->B[Metadata]
  B[Metadata]-->D[EndpointAPIDescriptionProvider]
  D[EndpointAPIDescriptionProvider]-->E[EndpointApiDescription]
  E[EndpointApiDescription]-->F[WithApiDescription]-->G[EndpointApiDescription]
  G[EndpointApiDescription]-->H[ApiDescription]
Loading

Unknowns

There are some considerations to be made around how this feature interacts with route groups. For now, it might be sufficient for our purposes to assume that the WithApiDescription method cannot be invoked on a RouteGroup since API-descriptions are endpoint-specific descriptors.

If we do want to provide support for WithApiDescription on route groups, we can work with one of two patterns:

  • The delegate registered via WithApiDescription is invoked on every endpoint defined within a group
  • The route group API provides a strategy for applying a callback on a single endpoint within a group

Alternatives Considered

Another design considered for this problem was a more builder-style approach for the WithApiDescription approach where in users could modify the API description using a set of ConfigureX methods.

app.MapPut(
  "/api/todos/{id}",
  (int id, [Description("This is a default")] Todo updatedTodo, TodosService todosService) => {
  	todosService.Update(id, updatedTodo);
	})
  .WithName("GetFoo")
  .WithDescription("This is an endpoint for updating a todo")
  // Builder pattern
  .WithApiDescription(builder => {
    builder.ConfigureParameter("updatedTodo", parameterBuilder => {
      parameterBuilder.AddExample(new Todo { ... })
      parameterBuilder.SetDescription("New description")
    });
    builder.ConfigureParameter("id", parameterBuilder => {
      	parameterBuilder.SetDescription("The id associated with the todo")
    });
    builder.ConfigureResponse(StatusCodes.200OK, responseBuilder => {
      responseBuilder.AddExample(new Todo { ... });
    })
    builder.ConfigureEndpoint(endpointBuilder => {
       endpointBuilder.SetName("GetBar");
       endpointBuilder.SetDescription("New Description");
    });
  });

Although this alternative does come with benefits, including, some of the drawbacks include:

  • Needing to define a new builder type for the API
  • The API itself is not particularly ergonomic
  • The experience for mutating existing properties is a little opaque
@captainsafia captainsafia added Needs: Design This issue requires design work before implementating. enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-openapi old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Feb 9, 2022
@captainsafia captainsafia added this to the 7.0-preview3 milestone Feb 9, 2022
@captainsafia captainsafia self-assigned this Feb 9, 2022
@captainsafia captainsafia removed the Needs: Design This issue requires design work before implementating. label Mar 1, 2022
@rafikiassumani-msft rafikiassumani-msft added Cost:M Priority:1 Work that is critical for the release, but we could probably ship without labels Mar 10, 2022
@captainsafia
Copy link
Member Author

Proposed API

We introduce a new collection of base types for describing an endpoint, its parameters, and its responses.

namespace Microsoft.AspNetCore.Http;

public class EndpointApiDescription
{
    public string EndpointName { get; set; }
    public string? GroupName { get; set; }
    public string HttpMethod { get; set; }
    public string? EndpointDescription { get; set; }
    public string[]? EndpointTags {get; set; }
    public EndpointMetadataCollection Metadata { get; set; }
    public Dictionary<string, EndpointApiParameter> Parameters = new();
    public Dictionary<int, EndpointApiResponse> Responses = new();
    public Dictionary<string, object?> Items = new();
}

public class EndpointApiParameter
{
    public string Name { get; set; }
    public Type ParameterType { get; set; }
    public string? Description { get; set; }
    public ParameterSource Source { get; set; }
    public ParameterInfo ParameterInfo { get; set; }
    public Dictionary<string, object?> Items = new();
    public IReadOnlyList<string>? ContentTypes { get; set; }
}

public class EndpointApiResponse
{
    public int? StatusCode { get; set; }
    public string? Description { get; set; }
    public Type? ResponseType { get; set; }
    public IReadOnlyList<string>? ContentTypes { get; set; }
    public Dictionary<string, object?> Items = new();
}

We also introduce a new ParameterSource type, meant to be a simpler, non-MVC substitute for the BindingSource type in MVC.

namespace Microsoft.AspNetCore.Http;
public enum ParameterSource
{
    Path,
    Query,
    Header,
    Body,
    FormFile,
    Services
}

Finally, we provide an extension method for adding a delegate to modify the EndpointApiDescription.

public static RouteHandlerBuilder WithApiDescription(this RouteHandlerBuilder builder, Action<EndpointApiDescription> configureDescription)
{
  builder.WithMetadata(configureDescription);
  return builder;
}

Usage Examples

app.MapGet("/todos/{id}", (int id) => ...)
  .WithApiDescription(endpointApiDescription =>
  {
     apiDescription.Description = "This is a new description."
     apiDescription.Parameters["id"].Items["Examples"] = 0;
     apiDescription.Responses[200].ResponseType = typeof(Todo);
  });

Alternative Designs

#40676 outlines an alternative proposal for this API where instead of using the new types that have been defined above, we use the types defined in OpenAPI.NET library and all users to modify those as part of the syntax.

Assuming we decide to pursue that proposal, the new APIs introduced here would be rendered moot. However, the new API will still allow us to validate whether or not this direction (or the alternative referenced) is the right direction to pursue.

@captainsafia captainsafia added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Mar 14, 2022
@ghost
Copy link

ghost commented Mar 14, 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.

@davidfowl
Copy link
Member

What is the Items bag and why is it string -> object instead of object -> object

@captainsafia
Copy link
Member Author

Closing to pursue bigger dreams.

@mkArtakMSFT
Copy link
Member

@captainsafia do you still expect for this proposal to be reviewed? If not, let's remove the api-ready-for-review label.

@captainsafia captainsafia removed the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Mar 21, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Apr 20, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement This issue represents an ask for new feature or an enhancement to an existing one 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:1 Work that is critical for the release, but we could probably ship without
Projects
None yet
Development

No branches or pull requests

4 participants