Skip to content

Commit 2d47d49

Browse files
Add OpenAPI operation and schema transformer interfaces (#56395)
* Add operation and schema transformer interfaces - Add `IOpenApiOperationTransformer`. - Add `IOpenApiSchemaTransformer`. - Rename `Use*Transfomer()` methods to `Add*Transformer()`. Resolves #56022. * Add operation and schema transform examples Add examples for DI-activated operation and schema transformers. * Fix rebase Fix mistake during rebase. * Run operation transforms before documents Run operation transformers before any document transformers. * Move transformer Add type-based transformer for operations to its own class. * Revert submodule Revert accidental change to submodule during rebase. * Apply review feedback - Remove collection expression. - Fix two typos. - Add parameter to benchmark.
1 parent 13d056a commit 2d47d49

25 files changed

+1191
-115
lines changed

AspNetCore.sln

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,7 +1788,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotReferencedInWasmCodePack
17881788
EndProject
17891789
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.WasmRemoteAuthentication", "src\Components\test\testassets\Components.WasmRemoteAuthentication\Components.WasmRemoteAuthentication.csproj", "{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}"
17901790
EndProject
1791-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "src\OpenApi\sample\Sample.csproj", "{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}"
1791+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "src\OpenApi\sample\Sample.csproj", "{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}"
17921792
EndProject
17931793
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hybrid", "Hybrid", "{2D64CA23-6E81-488E-A7D3-9BDF87240098}"
17941794
EndProject
@@ -1802,7 +1802,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Cachin
18021802
EndProject
18031803
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{9DC6B242-457B-4767-A84B-C3D23B76C642}"
18041804
EndProject
1805-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Microbenchmarks", "src\OpenApi\perf\Microbenchmarks\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj", "{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}"
1805+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OpenApi.Microbenchmarks", "src\OpenApi\perf\Microbenchmarks\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj", "{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}"
18061806
EndProject
18071807
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeyManagementSimulator", "src\DataProtection\samples\KeyManagementSimulator\KeyManagementSimulator.csproj", "{5B5F86CC-3598-463C-9F9B-F78FBB6642F4}"
18081808
EndProject

src/OpenApi/OpenApi.slnf

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22
"solution": {
33
"path": "..\\..\\AspNetCore.sln",
44
"projects": [
5+
"src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
56
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
67
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
8+
"src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
79
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
810
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
11+
"src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
912
"src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
13+
"src\\OpenApi\\perf\\Microbenchmarks\\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj",
14+
"src\\OpenApi\\sample\\Sample.csproj",
1015
"src\\OpenApi\\src\\Microsoft.AspNetCore.OpenApi.csproj",
1116
"src\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.Tests.csproj",
12-
"src\\OpenApi\\sample\\Sample.csproj",
13-
"src\\OpenApi\\perf\\Microbenchmarks\\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj"
17+
"src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj"
1418
]
1519
}
1620
}

src/OpenApi/perf/Microbenchmarks/TransformersBenchmark.cs

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,27 @@ public class TransformersBenchmark : OpenApiDocumentServiceTestBase
2121
public int TransformerCount { get; set; }
2222

2323
private readonly IEndpointRouteBuilder _builder = CreateBuilder();
24-
private readonly OpenApiOptions _options = new OpenApiOptions();
24+
private readonly OpenApiOptions _options = new();
2525
private OpenApiDocumentService _documentService;
2626

27+
[GlobalSetup(Target = nameof(ActivatedOperationTransformer))]
28+
public void ActivatedOperationTransformer_Setup()
29+
{
30+
_builder.MapGet("/", () => { });
31+
for (var i = 0; i <= TransformerCount; i++)
32+
{
33+
_options.AddOperationTransformer<OperationTransformer>();
34+
}
35+
_documentService = CreateDocumentService(_builder, _options);
36+
}
37+
2738
[GlobalSetup(Target = nameof(OperationTransformerAsDelegate))]
2839
public void OperationTransformerAsDelegate_Setup()
2940
{
3041
_builder.MapGet("/", () => { });
3142
for (var i = 0; i <= TransformerCount; i++)
3243
{
33-
_options.UseOperationTransformer((operation, context, token) =>
44+
_options.AddOperationTransformer((operation, context, token) =>
3445
{
3546
operation.Description = "New Description";
3647
return Task.CompletedTask;
@@ -45,7 +56,7 @@ public void ActivatedDocumentTransformer_Setup()
4556
_builder.MapGet("/", () => { });
4657
for (var i = 0; i <= TransformerCount; i++)
4758
{
48-
_options.UseTransformer<ActivatedTransformer>();
59+
_options.AddDocumentTransformer<DocumentTransformer>();
4960
}
5061
_documentService = CreateDocumentService(_builder, _options);
5162
}
@@ -56,7 +67,7 @@ public void DocumentTransformerAsDelegate_Delegate()
5667
_builder.MapGet("/", () => { });
5768
for (var i = 0; i <= TransformerCount; i++)
5869
{
59-
_options.UseTransformer((document, context, token) =>
70+
_options.AddDocumentTransformer((document, context, token) =>
6071
{
6172
document.Info.Description = "New Description";
6273
return Task.CompletedTask;
@@ -65,13 +76,24 @@ public void DocumentTransformerAsDelegate_Delegate()
6576
_documentService = CreateDocumentService(_builder, _options);
6677
}
6778

68-
[GlobalSetup(Target = nameof(SchemaTransformer))]
79+
[GlobalSetup(Target = nameof(ActivatedSchemaTransformer))]
80+
public void ActivatedSchemaTransformer_Setup()
81+
{
82+
_builder.MapPost("/", (Todo todo) => todo);
83+
for (var i = 0; i <= TransformerCount; i++)
84+
{
85+
_options.AddSchemaTransformer<SchemaTransformer>();
86+
}
87+
_documentService = CreateDocumentService(_builder, _options);
88+
}
89+
90+
[GlobalSetup(Target = nameof(SchemaTransformerAsDelegate))]
6991
public void SchemaTransformer_Setup()
7092
{
7193
_builder.MapPost("/", (Todo todo) => todo);
7294
for (var i = 0; i <= TransformerCount; i++)
7395
{
74-
_options.UseSchemaTransformer((schema, context, token) =>
96+
_options.AddSchemaTransformer((schema, context, token) =>
7597
{
7698
if (context.Type == typeof(Todo) && context.ParameterDescription != null)
7799
{
@@ -87,6 +109,12 @@ public void SchemaTransformer_Setup()
87109
_documentService = CreateDocumentService(_builder, _options);
88110
}
89111

112+
[Benchmark]
113+
public async Task ActivatedOperationTransformer()
114+
{
115+
await _documentService.GetOpenApiDocumentAsync();
116+
}
117+
90118
[Benchmark]
91119
public async Task OperationTransformerAsDelegate()
92120
{
@@ -106,17 +134,48 @@ public async Task DocumentTransformerAsDelegate()
106134
}
107135

108136
[Benchmark]
109-
public async Task SchemaTransformer()
137+
public async Task ActivatedSchemaTransformer()
138+
{
139+
await _documentService.GetOpenApiDocumentAsync();
140+
}
141+
142+
[Benchmark]
143+
public async Task SchemaTransformerAsDelegate()
110144
{
111145
await _documentService.GetOpenApiDocumentAsync();
112146
}
113147

114-
private class ActivatedTransformer : IOpenApiDocumentTransformer
148+
private class DocumentTransformer : IOpenApiDocumentTransformer
115149
{
116150
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
117151
{
118152
document.Info.Description = "Info Description";
119153
return Task.CompletedTask;
120154
}
121155
}
156+
157+
private class OperationTransformer : IOpenApiOperationTransformer
158+
{
159+
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
160+
{
161+
operation.Description = "Operation Description";
162+
return Task.CompletedTask;
163+
}
164+
}
165+
166+
private class SchemaTransformer : IOpenApiSchemaTransformer
167+
{
168+
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
169+
{
170+
if (context.Type == typeof(Todo) && context.ParameterDescription != null)
171+
{
172+
schema.Extensions["x-my-extension"] = new OpenApiString(context.ParameterDescription.Name);
173+
}
174+
else
175+
{
176+
schema.Extensions["x-my-extension"] = new OpenApiString("response");
177+
}
178+
return Task.CompletedTask;
179+
}
180+
}
122181
}

src/OpenApi/sample/Program.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.Frozen;
54
using System.Collections.Immutable;
65
using System.ComponentModel;
76
using Microsoft.AspNetCore.Http.HttpResults;
@@ -19,11 +18,13 @@
1918
builder.Services.AddOpenApi("v1", options =>
2019
{
2120
options.AddHeader("X-Version", "1.0");
22-
options.UseTransformer<BearerSecuritySchemeTransformer>();
21+
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
2322
});
2423
builder.Services.AddOpenApi("v2", options => {
25-
options.UseTransformer(new AddContactTransformer());
26-
options.UseTransformer((document, context, token) => {
24+
options.AddSchemaTransformer<AddExternalDocsTransformer>();
25+
options.AddOperationTransformer<AddExternalDocsTransformer>();
26+
options.AddDocumentTransformer(new AddContactTransformer());
27+
options.AddDocumentTransformer((document, context, token) => {
2728
document.Info.License = new OpenApiLicense { Name = "MIT" };
2829
return Task.CompletedTask;
2930
});
@@ -82,7 +83,8 @@
8283
v2.MapGet("/users", () => new [] { "alice", "bob" })
8384
.WithTags("users");
8485

85-
v2.MapPost("/users", () => Results.Created("/users/1", new { Id = 1, Name = "Test user" }));
86+
v2.MapPost("/users", () => Results.Created("/users/1", new { Id = 1, Name = "Test user" }))
87+
.WithName("CreateUser");
8688

8789
responses.MapGet("/200-add-xml", () => new TodoWithDueDate(1, "Test todo", false, DateTime.Now.AddDays(1), DateTime.Now))
8890
.Produces<Todo>(additionalContentTypes: "text/xml");
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.OpenApi;
5+
using Microsoft.OpenApi.Models;
6+
7+
namespace Sample.Transformers;
8+
9+
public sealed class AddExternalDocsTransformer(IConfiguration configuration) : IOpenApiOperationTransformer, IOpenApiSchemaTransformer
10+
{
11+
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
12+
{
13+
if (operation.OperationId is { Length: > 0 } id &&
14+
Uri.TryCreate(configuration["DocumentationBaseUrl"], UriKind.Absolute, out var baseUri))
15+
{
16+
var url = new Uri(baseUri, $"/api/docs/operations/{Uri.EscapeDataString(id)}");
17+
18+
operation.ExternalDocs = new OpenApiExternalDocs
19+
{
20+
Description = "Documentation for this OpenAPI endpoint",
21+
Url = url
22+
};
23+
}
24+
25+
return Task.CompletedTask;
26+
}
27+
28+
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
29+
{
30+
if (Uri.TryCreate(configuration["DocumentationBaseUrl"], UriKind.Absolute, out var baseUri))
31+
{
32+
var url = new Uri(baseUri, $"/api/docs/schemas/{Uri.EscapeDataString(schema.Type)}");
33+
34+
schema.ExternalDocs = new OpenApiExternalDocs
35+
{
36+
Description = "Documentation for this OpenAPI schema",
37+
Url = url
38+
};
39+
}
40+
return Task.CompletedTask;
41+
}
42+
}

src/OpenApi/sample/Transformers/OperationTransformers.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.OpenApi;
5-
using Microsoft.OpenApi.Models;
65
using Microsoft.OpenApi.Any;
76
using Microsoft.OpenApi.Extensions;
7+
using Microsoft.OpenApi.Models;
88

99
namespace Sample.Transformers;
1010

1111
public static class OperationTransformers
1212
{
1313
public static OpenApiOptions AddHeader(this OpenApiOptions options, string headerName, string defaultValue)
1414
{
15-
return options.UseOperationTransformer((operation, context, cancellationToken) =>
15+
return options.AddOperationTransformer((operation, context, cancellationToken) =>
1616
{
1717
var schema = OpenApiTypeMapper.MapTypeToOpenApiPrimitiveType(typeof(string));
1818
schema.Default = new OpenApiString(defaultValue);

src/OpenApi/sample/appsettings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"DocumentationBaseUrl": "https://example.com",
23
"Logging": {
34
"LogLevel": {
45
"Default": "Information",

src/OpenApi/src/PublicAPI.Unshipped.txt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
#nullable enable
22
Microsoft.AspNetCore.Builder.OpenApiEndpointRouteBuilderExtensions
33
Microsoft.AspNetCore.OpenApi.IOpenApiDocumentTransformer.TransformAsync(Microsoft.OpenApi.Models.OpenApiDocument! document, Microsoft.AspNetCore.OpenApi.OpenApiDocumentTransformerContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
4+
Microsoft.AspNetCore.OpenApi.IOpenApiOperationTransformer
5+
Microsoft.AspNetCore.OpenApi.IOpenApiOperationTransformer.TransformAsync(Microsoft.OpenApi.Models.OpenApiOperation! operation, Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
6+
Microsoft.AspNetCore.OpenApi.IOpenApiSchemaTransformer
7+
Microsoft.AspNetCore.OpenApi.IOpenApiSchemaTransformer.TransformAsync(Microsoft.OpenApi.Models.OpenApiSchema! schema, Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
48
Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Description.get -> Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescription!
59
Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Description.init -> void
610
Microsoft.AspNetCore.OpenApi.OpenApiOptions
11+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddOperationTransformer(Microsoft.AspNetCore.OpenApi.IOpenApiOperationTransformer! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
12+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddOperationTransformer<TTransformerType>() -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
13+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddSchemaTransformer(Microsoft.AspNetCore.OpenApi.IOpenApiSchemaTransformer! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
14+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddSchemaTransformer<TTransformerType>() -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
715
Microsoft.AspNetCore.OpenApi.OpenApiOptions.CreateSchemaReferenceId.get -> System.Func<System.Text.Json.Serialization.Metadata.JsonTypeInfo!, string?>!
816
Microsoft.AspNetCore.OpenApi.OpenApiOptions.CreateSchemaReferenceId.set -> void
917
Microsoft.AspNetCore.OpenApi.OpenApiOptions.DocumentName.get -> string!
@@ -12,11 +20,11 @@ Microsoft.AspNetCore.OpenApi.OpenApiOptions.OpenApiVersion.get -> Microsoft.Open
1220
Microsoft.AspNetCore.OpenApi.OpenApiOptions.OpenApiVersion.set -> void
1321
Microsoft.AspNetCore.OpenApi.OpenApiOptions.ShouldInclude.get -> System.Func<Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescription!, bool>!
1422
Microsoft.AspNetCore.OpenApi.OpenApiOptions.ShouldInclude.set -> void
15-
Microsoft.AspNetCore.OpenApi.OpenApiOptions.UseOperationTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiOperation!, Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
16-
Microsoft.AspNetCore.OpenApi.OpenApiOptions.UseSchemaTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiSchema!, Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
17-
Microsoft.AspNetCore.OpenApi.OpenApiOptions.UseTransformer(Microsoft.AspNetCore.OpenApi.IOpenApiDocumentTransformer! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
18-
Microsoft.AspNetCore.OpenApi.OpenApiOptions.UseTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiDocument!, Microsoft.AspNetCore.OpenApi.OpenApiDocumentTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
19-
Microsoft.AspNetCore.OpenApi.OpenApiOptions.UseTransformer<TTransformerType>() -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
23+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddOperationTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiOperation!, Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
24+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddSchemaTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiSchema!, Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
25+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddDocumentTransformer(Microsoft.AspNetCore.OpenApi.IOpenApiDocumentTransformer! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
26+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddDocumentTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiDocument!, Microsoft.AspNetCore.OpenApi.OpenApiDocumentTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
27+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddDocumentTransformer<TTransformerType>() -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
2028
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext
2129
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.ApplicationServices.get -> System.IServiceProvider!
2230
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.ApplicationServices.init -> void

0 commit comments

Comments
 (0)