Skip to content

Add APIs for OpenAPI document transformers #54935

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

Merged
merged 10 commits into from
Apr 8, 2024

Conversation

captainsafia
Copy link
Member

This PR adds support for document and operation transformers to OpenAPI feature branch. I've not included schema transformers in this delta since schema-related changes are not in.

@captainsafia captainsafia requested review from a team and wtgodbe as code owners April 3, 2024 23:26
@ghost ghost 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 Apr 3, 2024
@captainsafia captainsafia added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Apr 3, 2024
Copy link
Contributor

@Rick-Anderson Rick-Anderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trusting src/OpenApi/perf/TransformersBenchmark.cs is self documenting and doesn't need any comments.

{
if (_documentTransformer != null)
{
await _documentTransformer(document, context, cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could just return here as both won't be non-null?

@captainsafia
Copy link
Member Author

I'm trusting src/OpenApi/perf/TransformersBenchmark.cs is self documenting and doesn't need any comments.

I added a doc string to the benchmark class to describe what it's checking for.

_documentService = CreateDocumentService(_builder, _options);
}

[Benchmark]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What were the results? Are the comparable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example results are below. The OperationTransformerAsDelegate is more allocate-y than the others because of the use of the enumerator over the OpenApiOperations dictionary. We could optimize this but it makes the code a little less easy to read so I avoided this for now.

Method InvocationCount Mean Error StdDev Op/s Gen0 Gen1 Gen2 Allocated
OperationTransformerAsDelegate 10 502.24 us 2.784 us 2.604 us 1,991.07 3.9063 0.9766 0.9766 436.21 KB
ActivatedDocumentTransformer 10 50.65 us 0.510 us 0.477 us 19,744.86 0.3052 0.1221 0.0610 44.72 KB
DocumentTransformerAsDelegate 10 112.61 us 0.952 us 0.891 us 8,879.85 0.3662 0.2441 0.1221 44.72 KB
OperationTransformerAsDelegate 100 5,154.06 us 99.184 us 92.777 us 194.02 31.2500 - - 4282.05 KB
ActivatedDocumentTransformer 100 485.27 us 3.190 us 2.984 us 2,060.69 2.9297 0.9766 - 367.19 KB
DocumentTransformerAsDelegate 100 1,118.66 us 4.202 us 3.725 us 893.93 1.9531 - - 367.19 KB
OperationTransformerAsDelegate 1000 49,849.30 us 358.614 us 299.459 us 20.06 272.7273 - - 42820.56 KB
ActivatedDocumentTransformer 1000 4,973.16 us 61.507 us 57.534 us 201.08 23.4375 7.8125 - 3671.91 KB
DocumentTransformerAsDelegate 1000 11,212.38 us 81.451 us 76.189 us 89.19 15.6250 - - 3671.91 KB

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we expect that one to be widely used? I thought the whole point of exposing the helper was so that app developers wouldn't have to roll their own. I think it might be worth a reduction in readability in this case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I included the optimization in 40a9912. Description of it is here.

Original (with Dictionary enumerator)

Method TransformerCount Mean Error StdDev Op/s Gen0 Gen1 Allocated
OperationTransformerAsDelegate 10 2.156 us 0.0176 us 0.0147 us 463,813.7 0.0343 0.0076 4.15 KB
ActivatedDocumentTransformer 10 1.686 us 0.0216 us 0.0202 us 592,946.5 0.0324 0.0076 3.93 KB
DocumentTransformerAsDelegate 10 1.661 us 0.0177 us 0.0157 us 601,973.3 0.0305 0.0076 3.67 KB
OperationTransformerAsDelegate 100 7.310 us 0.0464 us 0.0387 us 136,796.5 0.0610 0.0153 7.66 KB
ActivatedDocumentTransformer 100 3.801 us 0.0311 us 0.0276 us 263,095.2 0.0496 0.0114 6.04 KB
DocumentTransformerAsDelegate 100 2.838 us 0.0337 us 0.0315 us 352,392.1 0.0305 0.0076 3.67 KB
OperationTransformerAsDelegate 1000 53.321 us 0.1793 us 0.1590 us 18,754.3 0.3052 0.0610 42.82 KB
ActivatedDocumentTransformer 1000 32.117 us 0.6423 us 0.6872 us 31,136.6 0.1831 - 28.13 KB
DocumentTransformerAsDelegate 1000 12.615 us 0.0257 us 0.0201 us 79,269.2 0.0153 - 3.67 KB

Improved (with OperationType-lookup)

Method TransformerCount Mean Error StdDev Op/s Gen0 Gen1 Allocated
OperationTransformerAsDelegate 10 2.561 us 0.0293 us 0.0274 us 390,501.5 0.0305 0.0076 3.7 KB
ActivatedDocumentTransformer 10 1.686 us 0.0108 us 0.0096 us 593,113.3 0.0324 0.0076 3.93 KB
DocumentTransformerAsDelegate 10 1.794 us 0.0134 us 0.0119 us 557,380.3 0.0305 0.0076 3.67 KB
OperationTransformerAsDelegate 100 8.255 us 0.1338 us 0.1252 us 121,144.2 0.0153 - 3.7 KB
ActivatedDocumentTransformer 100 4.016 us 0.0770 us 0.0824 us 248,976.9 0.0458 0.0076 6.04 KB
DocumentTransformerAsDelegate 100 2.762 us 0.0455 us 0.0426 us 362,068.0 0.0267 0.0076 3.67 KB
OperationTransformerAsDelegate 1000 60.630 us 0.1782 us 0.1667 us 16,493.6 - - 3.7 KB
ActivatedDocumentTransformer 1000 35.310 us 0.5062 us 0.4735 us 28,320.9 0.1831 - 27.13 KB
DocumentTransformerAsDelegate 1000 12.969 us 0.0904 us 0.0755 us 77,106.4 0.0153 - 3.67 KB


public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
Debug.Assert(Transformer != null, "Transformer should have been initialized.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is Initialize a separate method? Is TransformAsync expected to be called much more often?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we use null-coalescing to set the Transformer we could initialize it here but I liked the clarity that separating it out into separate calls provided. The fact that this transformer type needs to do some additional work at invocation to activate dependencies from the DI container felt special enough to call out in its own method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get it. Is it important that that happen after construction but before TransformAsync?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, following @BrennanConroy's feedback here, the Initialize method isn't the right way to go since we want scoped/transient services to be resolved again for each request/invocation to the document so this needs to be worked. Done in 73fc68a (#54935).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care about disposable transient transformers?
class MyTransformer : IDisposable, IOpenApiDocumentTransformer

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, see b7ad201.

[Benchmark]
public async Task ActivatedDocumentTransformer()
{
for (var i = 0; i < InvocationCount; i++)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an odd pattern, why are you doing it? The results for different invocation counts should just be the same relative to each other.

If you think an individual operation is too fast you can have a hardcoded invocation count and use [Benchmark(OperationsPerInvoke = 1000)]

[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if one's quadratic? 😝

@@ -43,7 +43,7 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e
}
else
{
var document = await documentService.GetOpenApiDocumentAsync();
var document = await documentService.GetOpenApiDocumentAsync(context.RequestAborted);
var documentOptions = options.Get(documentName);
using var output = MemoryBufferWriter.Get();
using var writer = Utf8BufferTextWriter.Get(output);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not part of this PR, but on line 53 you call output.ToArray() which seems wasteful, was BodyWriter.CopyFromAsync(output) evaluated?

}
finally
{
if (transformer is IDisposable disposable)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't these transformers effectively singletons because they come from the options object? So disposing them every use would be odd?


public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
_transformer = _transformerFactory.Invoke(context.ApplicationServices, []) as IOpenApiDocumentTransformer;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class can be hit in parallel by parallel requests, so the instance in the _transformer field isn't going to necessarily be preserved by the time DisposeAsync is called. You could just do the dispose inline and get rid of _transformer.

@captainsafia captainsafia requested a review from halter73 as a code owner April 5, 2024 23:34
Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks for the changes!

@captainsafia captainsafia merged commit b341f8c into feature/openapi Apr 8, 2024
@captainsafia captainsafia deleted the safia/openapi-transforms branch April 8, 2024 20:27
captainsafia added a commit that referenced this pull request Apr 18, 2024
Co-authored-by: Martin Costello <[email protected]>
Co-authored-by: Rick Anderson <[email protected]>

This PR adds support for OpenAPI document generation, sans schema generation to Microsoft.AspNetCore.OpenApi. Relevant changes are available in individual PRs:

- Add entry-point APIs for OpenAPI support (#54789)
- Support resolving OpenApiPaths entries from document (#54847) 
- Support generating OpenAPI operation and associated fields (#54903) 
- Add APIs for OpenAPI document transformers (#54935) 
- Add support for generating OpenAPI parameters (#55041)
- Add support for generating OpenAPI responses (#55020) 
- Add support for generating OpenAPI request bodies (#55040)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants