Skip to content

Commit 8b39e65

Browse files
authored
Apply schema transformers on properties and other subschemas (#56709)
* Apply schema transformers on properties and other subschemas * Add test for tranformers on unsupported subschemas * Avoid aggressive disposable for type-based schema transformers * Add some guards around recursion into properties and anyOf * Manage activated IOpenApiSchemaTransformer in OpenApiSchemaService
1 parent 6548eba commit 8b39e65

8 files changed

+437
-34
lines changed

src/OpenApi/perf/Microbenchmarks/TransformersBenchmark.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public void SchemaTransformer_Setup()
9595
{
9696
_options.AddSchemaTransformer((schema, context, token) =>
9797
{
98-
if (context.Type == typeof(Todo) && context.ParameterDescription != null)
98+
if (context.JsonTypeInfo.Type == typeof(Todo) && context.ParameterDescription != null)
9999
{
100100
schema.Extensions["x-my-extension"] = new OpenApiString(context.ParameterDescription.Name);
101101
}
@@ -167,7 +167,7 @@ private class SchemaTransformer : IOpenApiSchemaTransformer
167167
{
168168
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
169169
{
170-
if (context.Type == typeof(Todo) && context.ParameterDescription != null)
170+
if (context.JsonTypeInfo.Type == typeof(Todo) && context.ParameterDescription != null)
171171
{
172172
schema.Extensions["x-my-extension"] = new OpenApiString(context.ParameterDescription.Name);
173173
}

src/OpenApi/src/PublicAPI.Unshipped.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.ApplicationServices
3030
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.ApplicationServices.init -> void
3131
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.DocumentName.get -> string!
3232
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.DocumentName.init -> void
33+
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.JsonPropertyInfo.get -> System.Text.Json.Serialization.Metadata.JsonPropertyInfo?
34+
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.JsonPropertyInfo.init -> void
35+
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.JsonTypeInfo.get -> System.Text.Json.Serialization.Metadata.JsonTypeInfo!
36+
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.JsonTypeInfo.init -> void
3337
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.OpenApiSchemaTransformerContext() -> void
3438
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.ParameterDescription.get -> Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription?
3539
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.ParameterDescription.init -> void
36-
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.Type.get -> System.Type!
37-
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.Type.init -> void
3840
Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions
3941
static Microsoft.AspNetCore.Builder.OpenApiEndpointRouteBuilderExtensions.MapOpenApi(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern = "/openapi/{documentName}.json") -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
4042
static Microsoft.AspNetCore.OpenApi.OpenApiOptions.CreateDefaultSchemaReferenceId(System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo) -> string?

src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,17 +144,83 @@ internal async Task<OpenApiSchema> GetOrCreateSchemaAsync(Type type, ApiParamete
144144

145145
internal async Task ApplySchemaTransformersAsync(OpenApiSchema schema, Type type, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default)
146146
{
147+
var jsonTypeInfo = _jsonSerializerOptions.GetTypeInfo(type);
147148
var context = new OpenApiSchemaTransformerContext
148149
{
149150
DocumentName = documentName,
150-
Type = type,
151+
JsonTypeInfo = jsonTypeInfo,
152+
JsonPropertyInfo = null,
151153
ParameterDescription = parameterDescription,
152154
ApplicationServices = serviceProvider
153155
};
154156
for (var i = 0; i < _openApiOptions.SchemaTransformers.Count; i++)
155157
{
156158
var transformer = _openApiOptions.SchemaTransformers[i];
157-
await transformer.TransformAsync(schema, context, cancellationToken);
159+
// If the transformer is a type-based transformer, we need to initialize and finalize it
160+
// once in the context of the top-level assembly and not the child properties we are invoking
161+
// it on.
162+
if (transformer is TypeBasedOpenApiSchemaTransformer typeBasedTransformer)
163+
{
164+
var initializedTransformer = typeBasedTransformer.InitializeTransformer(serviceProvider);
165+
try
166+
{
167+
await InnerApplySchemaTransformersAsync(schema, jsonTypeInfo, context, initializedTransformer, cancellationToken);
168+
}
169+
finally
170+
{
171+
await TypeBasedOpenApiSchemaTransformer.FinalizeTransformer(initializedTransformer);
172+
}
173+
}
174+
else
175+
{
176+
await InnerApplySchemaTransformersAsync(schema, jsonTypeInfo, context, transformer, cancellationToken);
177+
}
178+
}
179+
}
180+
181+
private async Task InnerApplySchemaTransformersAsync(OpenApiSchema schema,
182+
JsonTypeInfo jsonTypeInfo,
183+
OpenApiSchemaTransformerContext context,
184+
IOpenApiSchemaTransformer transformer,
185+
CancellationToken cancellationToken = default)
186+
{
187+
await transformer.TransformAsync(schema, context, cancellationToken);
188+
189+
// Only apply transformers on polymorphic schemas where we can resolve the derived
190+
// types associated with the base type.
191+
if (schema.AnyOf is { Count: > 0 } && jsonTypeInfo.PolymorphismOptions is not null)
192+
{
193+
var anyOfIndex = 0;
194+
foreach (var derivedType in jsonTypeInfo.PolymorphismOptions.DerivedTypes)
195+
{
196+
var derivedJsonTypeInfo = _jsonSerializerOptions.GetTypeInfo(derivedType.DerivedType);
197+
context.UpdateJsonTypeInfo(derivedJsonTypeInfo, null);
198+
if (schema.AnyOf.Count <= anyOfIndex)
199+
{
200+
break;
201+
}
202+
await InnerApplySchemaTransformersAsync(schema.AnyOf[anyOfIndex], derivedJsonTypeInfo, context, transformer, cancellationToken);
203+
anyOfIndex++;
204+
}
205+
}
206+
207+
if (schema.Items is not null)
208+
{
209+
var elementTypeInfo = _jsonSerializerOptions.GetTypeInfo(jsonTypeInfo.ElementType!);
210+
context.UpdateJsonTypeInfo(elementTypeInfo, null);
211+
await InnerApplySchemaTransformersAsync(schema.Items, elementTypeInfo, context, transformer, cancellationToken);
212+
}
213+
214+
if (schema.Properties is { Count: > 0 })
215+
{
216+
foreach (var propertyInfo in jsonTypeInfo.Properties)
217+
{
218+
context.UpdateJsonTypeInfo(_jsonSerializerOptions.GetTypeInfo(propertyInfo.PropertyType), propertyInfo);
219+
if (schema.Properties.TryGetValue(propertyInfo.Name, out var propertySchema))
220+
{
221+
await InnerApplySchemaTransformersAsync(propertySchema, _jsonSerializerOptions.GetTypeInfo(propertyInfo.PropertyType), context, transformer, cancellationToken);
222+
}
223+
}
158224
}
159225
}
160226

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
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.Text.Json.Serialization.Metadata;
45
using Microsoft.AspNetCore.Mvc.ApiExplorer;
5-
using Microsoft.OpenApi.Models;
66

77
namespace Microsoft.AspNetCore.OpenApi;
88

@@ -11,24 +11,41 @@ namespace Microsoft.AspNetCore.OpenApi;
1111
/// </summary>
1212
public sealed class OpenApiSchemaTransformerContext
1313
{
14+
private JsonTypeInfo? _jsonTypeInfo;
15+
private JsonPropertyInfo? _jsonPropertyInfo;
16+
1417
/// <summary>
1518
/// Gets the name of the associated OpenAPI document.
1619
/// </summary>
1720
public required string DocumentName { get; init; }
1821

19-
/// <summary>
20-
/// Gets the <see cref="Type" /> associated with the current <see cref="OpenApiSchema"/>.
21-
/// </summary>
22-
public required Type Type { get; init; }
23-
2422
/// <summary>
2523
/// Gets the <see cref="ApiParameterDescription"/> associated with the target schema.
2624
/// Null when processing an OpenAPI schema for a response type.
2725
/// </summary>
2826
public required ApiParameterDescription? ParameterDescription { get; init; }
2927

28+
/// <summary>
29+
/// Gets the <see cref="JsonTypeInfo"/> associated with the target schema.
30+
/// </summary>
31+
public required JsonTypeInfo JsonTypeInfo { get => _jsonTypeInfo!; init => _jsonTypeInfo = value; }
32+
33+
/// <summary>
34+
/// Gets the <see cref="JsonPropertyInfo"/> associated with the target schema if the
35+
/// target schema is a property of a parent schema.
36+
/// </summary>
37+
public required JsonPropertyInfo? JsonPropertyInfo { get => _jsonPropertyInfo; init => _jsonPropertyInfo = value; }
38+
3039
/// <summary>
3140
/// Gets the application services associated with the current document the target schema is in.
3241
/// </summary>
3342
public required IServiceProvider ApplicationServices { get; init; }
43+
44+
// Expose internal setters for the properties that only allow initializations to avoid allocating
45+
// new instances of the context for each sub-schema transformation.
46+
internal void UpdateJsonTypeInfo(JsonTypeInfo jsonTypeInfo, JsonPropertyInfo? jsonPropertyInfo)
47+
{
48+
_jsonTypeInfo = jsonTypeInfo;
49+
_jsonPropertyInfo = jsonPropertyInfo;
50+
}
3451
}

src/OpenApi/src/Transformers/TypeBasedOpenApiSchemaTransformer.cs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,26 @@ internal TypeBasedOpenApiSchemaTransformer([DynamicallyAccessedMembers(Dynamical
2020
_transformerFactory = ActivatorUtilities.CreateFactory(_transformerType, []);
2121
}
2222

23-
public async Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
23+
internal IOpenApiSchemaTransformer InitializeTransformer(IServiceProvider serviceProvider)
2424
{
25-
var transformer = _transformerFactory.Invoke(context.ApplicationServices, []) as IOpenApiSchemaTransformer;
25+
var transformer = _transformerFactory.Invoke(serviceProvider, []) as IOpenApiSchemaTransformer;
2626
Debug.Assert(transformer != null, $"The type {_transformerType} does not implement {nameof(IOpenApiSchemaTransformer)}.");
27-
try
27+
return transformer;
28+
}
29+
30+
internal static async Task FinalizeTransformer(IOpenApiSchemaTransformer transformer)
31+
{
32+
if (transformer is IAsyncDisposable asyncDisposable)
2833
{
29-
await transformer.TransformAsync(schema, context, cancellationToken);
34+
await asyncDisposable.DisposeAsync();
3035
}
31-
finally
36+
else if (transformer is IDisposable disposable)
3237
{
33-
if (transformer is IAsyncDisposable asyncDisposable)
34-
{
35-
await asyncDisposable.DisposeAsync();
36-
}
37-
else if (transformer is IDisposable disposable)
38-
{
39-
disposable.Dispose();
40-
}
38+
disposable.Dispose();
4139
}
4240
}
41+
42+
// No-op because the activate instance is invoked by the OpenApiSchema service.
43+
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
44+
=> Task.CompletedTask;
4345
}

src/OpenApi/test/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v2.verified.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@
5252
"ArrayOfstring": {
5353
"type": "array",
5454
"items": {
55-
"type": "string"
55+
"type": "string",
56+
"externalDocs": {
57+
"description": "Documentation for this OpenAPI schema",
58+
"url": "https://example.com/api/docs/schemas/string"
59+
}
5660
},
5761
"externalDocs": {
5862
"description": "Documentation for this OpenAPI schema",

src/OpenApi/test/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ public async Task TypeModifiedWithSchemaTransformerMapsToDifferentReferenceId()
269269
var options = new OpenApiOptions();
270270
options.AddSchemaTransformer((schema, context, cancellationToken) =>
271271
{
272-
if (context.Type == typeof(Todo) && context.ParameterDescription is not null)
272+
if (context.JsonTypeInfo.Type == typeof(Todo) && context.ParameterDescription is not null)
273273
{
274274
schema.Extensions["x-my-extension"] = new OpenApiString(context.ParameterDescription.Name);
275275
}

0 commit comments

Comments
 (0)