Skip to content

Commit 62e16af

Browse files
committed
Apply schema transformers on properties and other subschemas
1 parent 2d47d49 commit 62e16af

File tree

7 files changed

+348
-24
lines changed

7 files changed

+348
-24
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: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,17 +144,58 @@ 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+
await InnerApplySchemaTransformersAsync(schema, jsonTypeInfo, context, transformer, cancellationToken);
160+
}
161+
}
162+
163+
private async Task InnerApplySchemaTransformersAsync(OpenApiSchema schema,
164+
JsonTypeInfo jsonTypeInfo,
165+
OpenApiSchemaTransformerContext context,
166+
IOpenApiSchemaTransformer transformer,
167+
CancellationToken cancellationToken = default)
168+
{
169+
await transformer.TransformAsync(schema, context, cancellationToken);
170+
171+
// Only apply transformers on polymorphic schemas where we can resolve the derived
172+
// types associated with the base type.
173+
if (schema.AnyOf is { Count: > 0 } && jsonTypeInfo.PolymorphismOptions is not null)
174+
{
175+
var anyOfIndex = 0;
176+
foreach (var derivedType in jsonTypeInfo.PolymorphismOptions.DerivedTypes)
177+
{
178+
var derivedJsonTypeInfo = _jsonSerializerOptions.GetTypeInfo(derivedType.DerivedType);
179+
context.UpdateJsonTypeInfo(derivedJsonTypeInfo, null);
180+
await InnerApplySchemaTransformersAsync(schema.AnyOf[anyOfIndex], derivedJsonTypeInfo, context, transformer, cancellationToken);
181+
anyOfIndex++;
182+
}
183+
}
184+
185+
if (schema.Items is not null)
186+
{
187+
var elementTypeInfo = _jsonSerializerOptions.GetTypeInfo(jsonTypeInfo.ElementType!);
188+
context.UpdateJsonTypeInfo(elementTypeInfo, null);
189+
await InnerApplySchemaTransformersAsync(schema.Items, elementTypeInfo, context, transformer, cancellationToken);
190+
}
191+
192+
if (schema.Properties is { Count: > 0 })
193+
{
194+
foreach (var propertyInfo in jsonTypeInfo.Properties)
195+
{
196+
context.UpdateJsonTypeInfo(_jsonSerializerOptions.GetTypeInfo(propertyInfo.PropertyType), propertyInfo);
197+
await InnerApplySchemaTransformersAsync(schema.Properties[propertyInfo.Name], jsonTypeInfo, context, transformer, cancellationToken);
198+
}
158199
}
159200
}
160201

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/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)