Skip to content

Commit 80435a6

Browse files
authored
Support generating OpenApiSchemas from transformers (#61050)
1 parent 335008a commit 80435a6

7 files changed

+664
-6
lines changed
+7
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
11
#nullable enable
22
static Microsoft.AspNetCore.Builder.OpenApiEndpointConventionBuilderExtensions.AddOpenApiOperationTransformer<TBuilder>(this TBuilder builder, System.Func<Microsoft.OpenApi.Models.OpenApiOperation!, Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> TBuilder
3+
Microsoft.AspNetCore.OpenApi.OpenApiDocumentTransformerContext.GetOrCreateSchemaAsync(System.Type! type, Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription? parameterDescription = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.OpenApi.Models.OpenApiSchema!>!
4+
Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Document.get -> Microsoft.OpenApi.Models.OpenApiDocument?
5+
Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Document.init -> void
6+
Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.GetOrCreateSchemaAsync(System.Type! type, Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription? parameterDescription = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.OpenApi.Models.OpenApiSchema!>!
7+
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.Document.get -> Microsoft.OpenApi.Models.OpenApiDocument?
8+
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.Document.init -> void
9+
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.GetOrCreateSchemaAsync(System.Type! type, Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription? parameterDescription = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.OpenApi.Models.OpenApiSchema!>!

src/OpenApi/src/Services/OpenApiDocumentService.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scop
7777
document.Paths = await GetOpenApiPathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
7878
try
7979
{
80-
await ApplyTransformersAsync(document, scopedServiceProvider, cancellationToken);
80+
await ApplyTransformersAsync(document, scopedServiceProvider, schemaTransformers, cancellationToken);
8181
}
8282

8383
finally
@@ -95,13 +95,15 @@ public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scop
9595
return document;
9696
}
9797

98-
private async Task ApplyTransformersAsync(OpenApiDocument document, IServiceProvider scopedServiceProvider, CancellationToken cancellationToken)
98+
private async Task ApplyTransformersAsync(OpenApiDocument document, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken)
9999
{
100100
var documentTransformerContext = new OpenApiDocumentTransformerContext
101101
{
102102
DocumentName = documentName,
103103
ApplicationServices = scopedServiceProvider,
104104
DescriptionGroups = apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items,
105+
Document = document,
106+
SchemaTransformers = schemaTransformers
105107
};
106108
// Use index-based for loop to avoid allocating an enumerator with a foreach.
107109
for (var i = 0; i < _options.DocumentTransformers.Count; i++)
@@ -271,6 +273,8 @@ private async Task<Dictionary<OperationType, OpenApiOperation>> GetOperationsAsy
271273
DocumentName = documentName,
272274
Description = description,
273275
ApplicationServices = scopedServiceProvider,
276+
Document = document,
277+
SchemaTransformers = schemaTransformers
274278
};
275279

276280
_operationTransformerContextCache.TryAdd(description.ActionDescriptor.Id, operationContext);

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

+12-4
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ internal sealed class OpenApiSchemaService(
118118
}
119119
};
120120

121-
internal async Task<IOpenApiSchema> GetOrCreateSchemaAsync(OpenApiDocument document, Type type, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default)
121+
internal async Task<OpenApiSchema> GetOrCreateUnresolvedSchemaAsync(OpenApiDocument? document, Type type, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default)
122122
{
123123
var key = parameterDescription?.ParameterDescriptor is IParameterInfoParameterDescriptor parameterInfoDescription
124124
&& parameterDescription.ModelMetadata.PropertyName is null
@@ -133,7 +133,13 @@ internal async Task<IOpenApiSchema> GetOrCreateSchemaAsync(OpenApiDocument docum
133133
var deserializedSchema = JsonSerializer.Deserialize(schemaAsJsonObject, _jsonSchemaContext.OpenApiJsonSchema);
134134
Debug.Assert(deserializedSchema != null, "The schema should have been deserialized successfully and materialize a non-null value.");
135135
var schema = deserializedSchema.Schema;
136-
await ApplySchemaTransformersAsync(schema, type, scopedServiceProvider, schemaTransformers, parameterDescription, cancellationToken);
136+
await ApplySchemaTransformersAsync(document, schema, type, scopedServiceProvider, schemaTransformers, parameterDescription, cancellationToken);
137+
return schema;
138+
}
139+
140+
internal async Task<IOpenApiSchema> GetOrCreateSchemaAsync(OpenApiDocument document, Type type, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default)
141+
{
142+
var schema = await GetOrCreateUnresolvedSchemaAsync(document, type, scopedServiceProvider, schemaTransformers, parameterDescription, cancellationToken);
137143
return ResolveReferenceForSchema(document, schema);
138144
}
139145

@@ -229,7 +235,7 @@ internal static IOpenApiSchema ResolveReferenceForSchema(OpenApiDocument documen
229235
return schema;
230236
}
231237

232-
internal async Task ApplySchemaTransformersAsync(IOpenApiSchema schema, Type type, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default)
238+
internal async Task ApplySchemaTransformersAsync(OpenApiDocument? document, IOpenApiSchema schema, Type type, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default)
233239
{
234240
if (schemaTransformers.Length == 0)
235241
{
@@ -242,7 +248,9 @@ internal async Task ApplySchemaTransformersAsync(IOpenApiSchema schema, Type typ
242248
JsonTypeInfo = jsonTypeInfo,
243249
JsonPropertyInfo = null,
244250
ParameterDescription = parameterDescription,
245-
ApplicationServices = scopedServiceProvider
251+
ApplicationServices = scopedServiceProvider,
252+
Document = document,
253+
SchemaTransformers = schemaTransformers
246254
};
247255
for (var i = 0; i < schemaTransformers.Length; i++)
248256
{

src/OpenApi/src/Transformers/OpenApiDocumentTransformerContext.cs

+32
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
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.Diagnostics;
45
using Microsoft.AspNetCore.Mvc.ApiExplorer;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.OpenApi.Models;
58

69
namespace Microsoft.AspNetCore.OpenApi;
710

@@ -24,4 +27,33 @@ public sealed class OpenApiDocumentTransformerContext
2427
/// Gets the application services associated with current document.
2528
/// </summary>
2629
public required IServiceProvider ApplicationServices { get; init; }
30+
31+
internal IOpenApiSchemaTransformer[] SchemaTransformers { get; init; } = [];
32+
33+
// Internal because we expect users to interact with the `Document` provided in
34+
// the `IOpenApiDocumentTransformer` itself instead of the context object.
35+
internal OpenApiDocument? Document { get; init; }
36+
37+
/// <summary>
38+
/// Gets or creates an <see cref="OpenApiSchema"/> for the specified type. Augments
39+
/// the schema with any <see cref="IOpenApiSchemaTransformer"/>s that are registered
40+
/// on the document. If <paramref name="parameterDescription"/> is not null, the schema will be
41+
/// augmented with the <see cref="ApiParameterDescription"/> information.
42+
/// </summary>
43+
/// <param name="type">The type for which the schema is being created.</param>
44+
/// <param name="parameterDescription">An optional parameter description to augment the schema.</param>
45+
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
46+
/// <returns>A task that represents the asynchronous operation, with a value of type <see cref="OpenApiSchema"/>.</returns>
47+
public Task<OpenApiSchema> GetOrCreateSchemaAsync(Type type, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default)
48+
{
49+
Debug.Assert(Document is not null, "Document should have been initialized by framework.");
50+
var schemaService = ApplicationServices.GetRequiredKeyedService<OpenApiSchemaService>(DocumentName);
51+
return schemaService.GetOrCreateUnresolvedSchemaAsync(
52+
document: Document,
53+
type: type,
54+
parameterDescription: parameterDescription,
55+
scopedServiceProvider: ApplicationServices,
56+
schemaTransformers: SchemaTransformers,
57+
cancellationToken: cancellationToken);
58+
}
2759
}

src/OpenApi/src/Transformers/OpenApiOperationTransformerContext.cs

+31
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Mvc.ApiExplorer;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.OpenApi.Models;
57

68
namespace Microsoft.AspNetCore.OpenApi;
79

@@ -24,4 +26,33 @@ public sealed class OpenApiOperationTransformerContext
2426
/// Gets the application services associated with the current document the target operation is in.
2527
/// </summary>
2628
public required IServiceProvider ApplicationServices { get; init; }
29+
30+
/// <summary>
31+
/// Gets the OpenAPI document the current endpoint belongs to.
32+
/// </summary>
33+
public OpenApiDocument? Document { get; init; }
34+
35+
internal IOpenApiSchemaTransformer[] SchemaTransformers { get; init; } = [];
36+
37+
/// <summary>
38+
/// Gets or creates an <see cref="OpenApiSchema"/> for the specified type. Augments
39+
/// the schema with any <see cref="IOpenApiSchemaTransformer"/>s that are registered
40+
/// on the document. If <paramref name="parameterDescription"/> is not null, the schema will be
41+
/// augmented with the <see cref="ApiParameterDescription"/> information.
42+
/// </summary>
43+
/// <param name="type">The type for which the schema is being created.</param>
44+
/// <param name="parameterDescription">An optional parameter description to augment the schema.</param>
45+
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
46+
/// <returns>A task that represents the asynchronous operation, with a value of type <see cref="OpenApiSchema"/>.</returns>
47+
public Task<OpenApiSchema> GetOrCreateSchemaAsync(Type type, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default)
48+
{
49+
var schemaService = ApplicationServices.GetRequiredKeyedService<OpenApiSchemaService>(DocumentName);
50+
return schemaService.GetOrCreateUnresolvedSchemaAsync(
51+
document: Document,
52+
type: type,
53+
parameterDescription: parameterDescription,
54+
scopedServiceProvider: ApplicationServices,
55+
schemaTransformers: SchemaTransformers,
56+
cancellationToken: cancellationToken);
57+
}
2758
}

src/OpenApi/src/Transformers/OpenApiSchemaTransformerContext.cs

+31
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Text.Json.Serialization.Metadata;
55
using Microsoft.AspNetCore.Mvc.ApiExplorer;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.OpenApi.Models;
68

79
namespace Microsoft.AspNetCore.OpenApi;
810

@@ -41,11 +43,40 @@ public sealed class OpenApiSchemaTransformerContext
4143
/// </summary>
4244
public required IServiceProvider ApplicationServices { get; init; }
4345

46+
/// <summary>
47+
/// Gets the OpenAPI document the current schema belongs to.
48+
/// </summary>
49+
public OpenApiDocument? Document { get; init; }
50+
4451
// Expose internal setters for the properties that only allow initializations to avoid allocating
4552
// new instances of the context for each sub-schema transformation.
4653
internal void UpdateJsonTypeInfo(JsonTypeInfo jsonTypeInfo, JsonPropertyInfo? jsonPropertyInfo)
4754
{
4855
_jsonTypeInfo = jsonTypeInfo;
4956
_jsonPropertyInfo = jsonPropertyInfo;
5057
}
58+
59+
internal IOpenApiSchemaTransformer[] SchemaTransformers { get; init; } = [];
60+
61+
/// <summary>
62+
/// Gets or creates an <see cref="OpenApiSchema"/> for the specified type. Augments
63+
/// the schema with any <see cref="IOpenApiSchemaTransformer"/>s that are registered
64+
/// on the document. If <paramref name="parameterDescription"/> is not null, the schema will be
65+
/// augmented with the <see cref="ApiParameterDescription"/> information.
66+
/// </summary>
67+
/// <param name="type">The type for which the schema is being created.</param>
68+
/// <param name="parameterDescription">An optional parameter description to augment the schema.</param>
69+
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
70+
/// <returns>A task that represents the asynchronous operation, with a value of type <see cref="OpenApiSchema"/>.</returns>
71+
public Task<OpenApiSchema> GetOrCreateSchemaAsync(Type type, ApiParameterDescription? parameterDescription = null, CancellationToken cancellationToken = default)
72+
{
73+
var schemaService = ApplicationServices.GetRequiredKeyedService<OpenApiSchemaService>(DocumentName);
74+
return schemaService.GetOrCreateUnresolvedSchemaAsync(
75+
document: Document,
76+
type: type,
77+
parameterDescription: parameterDescription,
78+
scopedServiceProvider: ApplicationServices,
79+
schemaTransformers: SchemaTransformers,
80+
cancellationToken: cancellationToken);
81+
}
5182
}

0 commit comments

Comments
 (0)