Skip to content

Commit 90a622d

Browse files
Add support for CreateSchemaReferenceId option (#56753)
* Add support for CreateSchemaReferenceId option * Apply suggestions from code review Co-authored-by: Martin Costello <martin@martincostello.com> * Add test for reference ID de-dupe with CreateSchemaReferenceId --------- Co-authored-by: Martin Costello <martin@martincostello.com>
1 parent cd24d14 commit 90a622d

File tree

5 files changed

+337
-9
lines changed

5 files changed

+337
-9
lines changed

src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ internal static void ApplyDefaultValue(this JsonNode schema, object? defaultValu
168168
/// </remarks>
169169
/// <param name="schema">The <see cref="JsonNode"/> produced by the underlying schema generator.</param>
170170
/// <param name="context">The <see cref="JsonSchemaExporterContext"/> associated with the <see paramref="schema"/>.</param>
171-
internal static void ApplyPrimitiveTypesAndFormats(this JsonNode schema, JsonSchemaExporterContext context)
171+
/// <param name="createSchemaReferenceId">A delegate that generates the reference ID to create for a type.</param>
172+
internal static void ApplyPrimitiveTypesAndFormats(this JsonNode schema, JsonSchemaExporterContext context, Func<JsonTypeInfo, string?> createSchemaReferenceId)
172173
{
173174
var type = context.TypeInfo.Type;
174175
var underlyingType = Nullable.GetUnderlyingType(type);
@@ -177,7 +178,7 @@ internal static void ApplyPrimitiveTypesAndFormats(this JsonNode schema, JsonSch
177178
schema[OpenApiSchemaKeywords.NullableKeyword] = openApiSchema.Nullable || (schema[OpenApiSchemaKeywords.TypeKeyword] is JsonArray schemaType && schemaType.GetValues<string>().Contains("null"));
178179
schema[OpenApiSchemaKeywords.TypeKeyword] = openApiSchema.Type;
179180
schema[OpenApiSchemaKeywords.FormatKeyword] = openApiSchema.Format;
180-
schema[OpenApiConstants.SchemaId] = context.TypeInfo.GetSchemaReferenceId();
181+
schema[OpenApiConstants.SchemaId] = createSchemaReferenceId(context.TypeInfo);
181182
schema[OpenApiSchemaKeywords.NullableKeyword] = underlyingType != null;
182183
// Clear out patterns that the underlying JSON schema generator uses to represent
183184
// validations for DateTime, DateTimeOffset, and integers.
@@ -323,7 +324,8 @@ internal static void ApplyParameterInfo(this JsonNode schema, ApiParameterDescri
323324
/// </summary>
324325
/// <param name="schema">The <see cref="JsonNode"/> produced by the underlying schema generator.</param>
325326
/// <param name="context">The <see cref="JsonSchemaExporterContext"/> associated with the current type.</param>
326-
internal static void ApplyPolymorphismOptions(this JsonNode schema, JsonSchemaExporterContext context)
327+
/// <param name="createSchemaReferenceId">A delegate that generates the reference ID to create for a type.</param>
328+
internal static void ApplyPolymorphismOptions(this JsonNode schema, JsonSchemaExporterContext context, Func<JsonTypeInfo, string?> createSchemaReferenceId)
327329
{
328330
// The `context.Path.Length == 0` check is used to ensure that we only apply the polymorphism options
329331
// to the top-level schema and not to any nested schemas that are generated.
@@ -340,7 +342,7 @@ internal static void ApplyPolymorphismOptions(this JsonNode schema, JsonSchemaEx
340342
// that we hardcode here. We could use `OpenApiReference` to construct the reference and
341343
// serialize it but we use a hardcoded string here to avoid allocating a new object and
342344
// working around Microsoft.OpenApi's serialization libraries.
343-
mappings[$"{discriminator}"] = $"#/components/schemas/{context.TypeInfo.GetSchemaReferenceId()}{jsonDerivedType.GetSchemaReferenceId()}";
345+
mappings[$"{discriminator}"] = $"#/components/schemas/{createSchemaReferenceId(context.TypeInfo)}{createSchemaReferenceId(jsonDerivedType)}";
344346
}
345347
}
346348
schema[OpenApiSchemaKeywords.DiscriminatorKeyword] = polymorphismOptions.TypeDiscriminatorPropertyName;
@@ -353,9 +355,10 @@ internal static void ApplyPolymorphismOptions(this JsonNode schema, JsonSchemaEx
353355
/// </summary>
354356
/// <param name="schema">The <see cref="JsonNode"/> produced by the underlying schema generator.</param>
355357
/// <param name="context">The <see cref="JsonSchemaExporterContext"/> associated with the current type.</param>
356-
internal static void ApplySchemaReferenceId(this JsonNode schema, JsonSchemaExporterContext context)
358+
/// <param name="createSchemaReferenceId">A delegate that generates the reference ID to create for a type.</param>
359+
internal static void ApplySchemaReferenceId(this JsonNode schema, JsonSchemaExporterContext context, Func<JsonTypeInfo, string?> createSchemaReferenceId)
357360
{
358-
if (context.TypeInfo.GetSchemaReferenceId() is { } schemaReferenceId)
361+
if (createSchemaReferenceId(context.TypeInfo) is { } schemaReferenceId)
359362
{
360363
schema[OpenApiConstants.SchemaId] = schemaReferenceId;
361364
}

src/OpenApi/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Microsoft.AspNetCore.OpenApi.IOpenApiDocumentTransformer.TransformAsync(Microsof
44
Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Description.get -> Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescription!
55
Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Description.init -> void
66
Microsoft.AspNetCore.OpenApi.OpenApiOptions
7+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.CreateSchemaReferenceId.get -> System.Func<System.Text.Json.Serialization.Metadata.JsonTypeInfo!, string?>!
8+
Microsoft.AspNetCore.OpenApi.OpenApiOptions.CreateSchemaReferenceId.set -> void
79
Microsoft.AspNetCore.OpenApi.OpenApiOptions.DocumentName.get -> string!
810
Microsoft.AspNetCore.OpenApi.OpenApiOptions.OpenApiOptions() -> void
911
Microsoft.AspNetCore.OpenApi.OpenApiOptions.OpenApiVersion.get -> Microsoft.OpenApi.OpenApiSpecVersion
@@ -27,6 +29,7 @@ Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.Type.get -> System.
2729
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.Type.init -> void
2830
Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions
2931
static Microsoft.AspNetCore.Builder.OpenApiEndpointRouteBuilderExtensions.MapOpenApi(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern = "/openapi/{documentName}.json") -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
32+
static Microsoft.AspNetCore.OpenApi.OpenApiOptions.CreateDefaultSchemaReferenceId(System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo) -> string?
3033
static Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions.AddOpenApi(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
3134
static Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions.AddOpenApi(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! documentName) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
3235
static Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions.AddOpenApi(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! documentName, System.Action<Microsoft.AspNetCore.OpenApi.OpenApiOptions!>! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!

src/OpenApi/src/Services/OpenApiOptions.cs

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

44
using System.Diagnostics.CodeAnalysis;
5+
using System.Text.Json.Serialization.Metadata;
56
using Microsoft.AspNetCore.Mvc.ApiExplorer;
67
using Microsoft.OpenApi;
78
using Microsoft.OpenApi.Models;
@@ -16,6 +17,13 @@ public sealed class OpenApiOptions
1617
internal readonly List<IOpenApiDocumentTransformer> DocumentTransformers = [];
1718
internal readonly List<Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task>> SchemaTransformers = [];
1819

20+
/// <summary>
21+
/// A default implementation for creating a schema reference ID for a given <see cref="JsonTypeInfo"/>.
22+
/// </summary>
23+
/// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo"/> associated with the schema we are generating a reference ID for.</param>
24+
/// <returns>The reference ID to use for the schema or <see langword="null"/> if the schema should always be inlined.</returns>
25+
public static string? CreateDefaultSchemaReferenceId(JsonTypeInfo jsonTypeInfo) => jsonTypeInfo.GetSchemaReferenceId();
26+
1927
/// <summary>
2028
/// Initializes a new instance of the <see cref="OpenApiOptions"/> class
2129
/// with the default <see cref="ShouldInclude"/> predicate.
@@ -40,6 +48,15 @@ public OpenApiOptions()
4048
/// </summary>
4149
public Func<ApiDescription, bool> ShouldInclude { get; set; }
4250

51+
/// <summary>
52+
/// A delegate to determine how reference IDs should be created for schemas associated with types in the given OpenAPI document.
53+
/// </summary>
54+
/// <remarks>
55+
/// The default implementation uses the <see cref="CreateDefaultSchemaReferenceId"/> method to generate reference IDs. When
56+
/// the provided delegate returns <see langword="null"/>, the schema associated with the <see cref="JsonTypeInfo"/> will always be inlined.
57+
/// </remarks>
58+
public Func<JsonTypeInfo, string?> CreateSchemaReferenceId { get; set; } = CreateDefaultSchemaReferenceId;
59+
4360
/// <summary>
4461
/// Registers a new document transformer on the current <see cref="OpenApiOptions"/> instance.
4562
/// </summary>

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,10 @@ internal sealed class OpenApiSchemaService(
8888
{
8989
schema = new JsonObject();
9090
}
91-
schema.ApplyPrimitiveTypesAndFormats(context);
92-
schema.ApplySchemaReferenceId(context);
93-
schema.ApplyPolymorphismOptions(context);
91+
var createSchemaReferenceId = optionsMonitor.Get(documentName).CreateSchemaReferenceId;
92+
schema.ApplyPrimitiveTypesAndFormats(context, createSchemaReferenceId);
93+
schema.ApplySchemaReferenceId(context, createSchemaReferenceId);
94+
schema.ApplyPolymorphismOptions(context, createSchemaReferenceId);
9495
if (context.PropertyInfo is { } jsonPropertyInfo)
9596
{
9697
// Short-circuit STJ's handling of nested properties, which uses a reference to the

0 commit comments

Comments
 (0)