-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Description
Background and Motivation
I'm opening this proposal after a conversation with @sander1095 in relation to #58723 and #58724.
Prior to .NET 9, we supported a WithOpenApi
extension method on that when invoked would generate an OpenApiOperation
, inject it into endpoint metadata, then rely on consuming OpenAPI implementations like Swashbuckle to pluck this OpenApiOperation
and integrate it into the document that was being generated.
When we introduced built-in OpenAPI support in .NET 9, we opted not to bring in support for this strategy and instead steer people towards the new IOpenApiOperationTransformer
abstraction for making modifications to their OpenAPI document.
However, one of the things that @sander1095 pointed out with this approach is that you lose the ability to have operation transformations colocated with the endpoint they affected. For example, let's say that I want to set the description for a response in a given endpoint. With the operation transformer model, I might have to write something like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(options =>
{
options.AddOperationTransformer((operation, context, ct) =>
{
if (context.Description.RelativePath == "weatherforecast")
{
operation.Responses["200"].Description = "Weather forecast for today";
}
return Task.CompletedTask;
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapGet("/weatherforecast", () =>
new WeatherForecast(DateOnly.FromDateTime(DateTime.Now), 25, "Sunny"));
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
In addition to the transformer being far away from the associated endpoint, I also have to implement the associated path-based check myself.
Proposed API
This issue proposes introducing a WithOpenApiTransformer
extension method that can be used to register an operation transformer for a given endpoint without having to use the global registration feature.
// Assembly: Microsoft.AspNetCore.OpenApi
namespace Microsoft.AspNetCore.Builder;
public static class OpenApiEndpointConventionBuilderExtensions
{
+ public static TBuilder AddOpenApiOperationTransformer<TBuilder>(this TBuilder builder, Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer) where TBuilder : IEndpointConventionBuilder { }
}
Usage Examples
With this proposed API, the same example above could be re-implemented in the following way:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapGet("/weatherforecast", () =>
new WeatherForecast(DateOnly.FromDateTime(DateTime.Now), 25, "Sunny"))
.AddOpenApiOperationTransformer((operation, context, ct) =>
{
operation.Responses["200"].Description = "Weather forecast for today";
return Task.CompletedTask;
});
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
There's also the option for 3rd party authors to provide their own extensions on top of this API to support further customizations. For example, the support for setting descriptions on responses as proposed in #58724 can be implemented in the following way:
namespace FooBar.OpenApiExtensions;
public static class ExtensionMethods
{
public static RouteHandlerBuilder WithResponseDescription(this RouteHandlerBuilder builder, int statusCode, string description)
{
builder.AddOpenApiOperationTransformer((operation, context, cancellationToken)
{
operation.Responses[statusCode.ToString()].Description = description;
return Task.CompletedTask;
});
return builder;
}
}
With the consumption pattern in the invoked code being:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapGet("/weatherforecast", () =>
new WeatherForecast(DateOnly.FromDateTime(DateTime.Now), 25, "Sunny"))
.WithResponseDescription(200, "Weather forecast for today");
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Alternative Designs
- We could instead update the implementation to support the pre-existing
WithOpenApi
overloads. However, this would prevent users from being able to access theOpenApiOperationTransformerContext
to customize the behavior of the transformer. - The proposal only supports adding the transformer via the delegate pattern. We could consider supporting the other registration patterns as well although that might not be necessary.
Risks
- This proposal does introduce additional complexity to the transformer registration APIs. Operation transformers become unique in the sense that they can be configured at the top-level via the options object and at the endpoint level. We'll also have to consider ordering semantics for when operation transformers are applied.
Addendum
In the first round of reviews on this API, there was feedback about whether it was feasible to reuse the existing WithOpenApi
to fulfill this feature. After some investigation, a new API is required because:
- Accessing the transformer context is useful for being able to more fully interact with the document, as is the case with Add API to support dyanmically generated OpenAPI schemas in document #60589.
- The existing
WithOpenApi
overloads follow last-one-wins semantics but we want this API to be additive.