question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Support `WithApiDescription` extension method for minimal APIs

See original GitHub issue

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]

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]

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

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:2
  • Comments:5 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
captainsafiacommented, Mar 16, 2022

Closing to pursue bigger dreams.

0reactions
mkArtakMSFTcommented, Mar 19, 2022

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

Minimal APIs quick reference
Provides an overview of minimal APIs in ASP. ... The MapGroup extension method helps organize groups of endpoints with a common prefix.
Read more >
How to Create Minimal APIs in .NET 6 | Extension Methods
Fritz is simplifying the content of an ASP.NET Core Minimal API by moving configuration of the API into extension methods.
Read more >
Exploring the new minimal API source generator
In this post I take a look at the new minimal API source generator added in .NET 8 preview 3 to support AOT,...
Read more >
Minimal APIs in .NET 6
In this article, we are going to explain the core idea and basic concepts of the minimal APIs in .NET 6 with examples....
Read more >
What's New in .NET 7 for Minimal APIs?
NET 7 arrived and brought three great features to minimal APIs. Check out in this blog post what's new with practical examples.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found