dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.29k stars 9.96k forks source link

Add support for an interface for OpenAPI operation transformations #56022

Closed martincostello closed 2 months ago

martincostello commented 4 months ago

Background and Motivation

In the new OpenAPI functionality in .NET 9 preview.4 there is a first-class interface for document transformations, IOpenApiDocumentTransformer, but there isn't an analogous interface for operations.

It is possible to perform operation transformations by providing a lambda function, but this can quickly get complicated if you want to do things that are non-trivial, use dependencies from DI etc. compared to if you could encapsulate your logic in a class to perform a specific operation like is possible for document transforms.

Proposed API

namespace Microsoft.AspNetCore.OpenApi;

+/// <summary>
+/// Represents a transformer that can be used to modify an OpenAPI operation.
+/// </summary>
+public interface IOpenApiOperationTransformer
+{
+    Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken);
+}

public partial sealed class OpenApiOptions
{
+    public OpenApiOptions UseOperationTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
+        where TTransformerType : IOpenApiOperationTransformer
+    public OpenApiOptions UseOperationTransformer(IOpenApiOperationTransformer transformer)
}

These would be implemented the same way the document support is, so that DI works as expected to activate the types, it's AoT-friendly etc.

Usage Examples

services.AddOpenApi("v1", (options) =>
{
    options.UseOperationTransformer<MyOperationTransformer>();
});

public sealed class MyOperationTransformer(MyService someService, IConfiguration configuration) : IOpenApiOperationTransformer
{
    public Task TransformAsync(
        OpenApiOperation operation,
        OpenApiOperationTransformerContext context,
        CancellationToken cancellationToken)
    {
        if (configuration["DoWhatever"] == "true")
        {
            await someService.DoStuff(operation, cancellationToken);
        }
    }
}

Alternative Designs

Make the code that enumerates the operations re-usable to public callers so that if an operation interface isn't desired, then the user can implement a document transformer to easily enumerate the operations to do a transform in their custom implementation of the interface. Maybe a base class that implements IOpenApiDocumentTransformer and applies the visitor pattern to allow the user to override methods to apply transforms:

namespace Microsoft.AspNetCore.OpenApi;

+public abstract class OpenApiOperationTransformer : IOpenApiDocumentTransformer
+{
+    protected OpenApiOperationTransformer();
+
+    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken); // Enumerates the operations and calls TransformOperationAsync() for each
+
+    protected abstract Task TransformOperationAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken);
}

Risks

Increased complexity of the OpenAPI generator implementation.

captainsafia commented 4 months ago

@martincostello Thanks for filing this issue!

We intentionally kept the surface area for the transformers API small to start, with the option to introduce an interface for operation transformers if the feedback arose. It appears the feedback has arisen.

The API that you've proposed is pretty close in shape to what was originally encapsulated in the API proposal for transformers. The only thing missing is a UseTransformer(IOpenApiOperationTransformer) registration overload. I'll add it to the proposal for completeness and we can discuss if it is necessary during API review.

dotnet-policy-service[bot] commented 4 months ago

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:

halter73 commented 3 months ago

API approved.

namespace Microsoft.AspNetCore.OpenApi;

+/// <summary>
+/// Represents a transformer that can be used to modify an OpenAPI operation.
+/// </summary>
+public interface IOpenApiOperationTransformer
+{
+    Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken);
+}

+ public interface IOpenApiSchemaTransformer
+ {
+  Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken);
+ }

public partial sealed class OpenApiOptions
{
+    public OpenApiOptions AddOperationTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
+        where TTransformerType : IOpenApiOperationTransformer
+    public OpenApiOptions AddOperationTransformer(IOpenApiOperationTransformer transformer)

-  public OpenApiOptions UseTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
-        where TTransformerType : IOpenApiDocumentTransformer
+  public OpenApiOptions AddDocumentTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
+        where TTransformerType : IOpenApiDocumentTransformer
-  public OpenApiOptions UseTransformer(IOpenApiDocumentTransformer transformer)
+  public OpenApiOptions AddDocumentTransformer(IOpenApiDocumentTransformer transformer)
-  public OpenApiOptions UseTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer);
+  public OpenApiOptions AddDocumentTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer);
-  public OpenApiOptions UseOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer);
+  public OpenApiOptions AddOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer);
-  public OpenApiOptions UseSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> transformer);
+  public OpenApiOptions AddSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> transformer);

+    public OpenApiOptions AddSchemaTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
+        where TTransformerType : IOpenApiSchemaTransformer
+    public OpenApiOptions AddSchemaTransformer(IOpenApiSchemaTransformer transformer);
}
martincostello commented 3 months ago

@captainsafia I'm happy to pick this up, unless you're planning on tackling it yourself.

captainsafia commented 3 months ago

@martincostello I'd be happy to review a PR if you're able to open one! 🙇🏽