Skip to content

Commit 53f01dc

Browse files
authored
Scrub extension properties in OpenAPI document during serialization (#56688)
1 parent 8ae09e8 commit 53f01dc

13 files changed

+128
-141
lines changed

src/OpenApi/src/Comparers/OpenApiAnyComparer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public bool Equals(IOpenApiAny? x, IOpenApiAny? y)
4242
OpenApiByte byteX => y is OpenApiByte byteY && byteX.Value.SequenceEqual(byteY.Value),
4343
OpenApiDate dateX => y is OpenApiDate dateY && dateX.Value == dateY.Value,
4444
OpenApiDateTime dateTimeX => y is OpenApiDateTime dateTimeY && dateTimeX.Value == dateTimeY.Value,
45+
ScrubbedOpenApiAny scrubbedX => y is ScrubbedOpenApiAny scrubbedY && scrubbedX.Value == scrubbedY.Value,
4546
_ => x.Equals(y)
4647
});
4748
}
@@ -73,6 +74,7 @@ public int GetHashCode(IOpenApiAny obj)
7374
OpenApiPassword password => password.Value,
7475
OpenApiDate date => date.Value,
7576
OpenApiDateTime dateTime => dateTime.Value,
77+
ScrubbedOpenApiAny scrubbed => scrubbed.Value,
7678
_ => null
7779
});
7880

src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using Microsoft.Extensions.DependencyInjection;
1010
using Microsoft.Extensions.Options;
1111
using Microsoft.OpenApi.Extensions;
12-
using Microsoft.OpenApi.Writers;
1312

1413
namespace Microsoft.AspNetCore.Builder;
1514

@@ -49,7 +48,7 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e
4948
using var writer = Utf8BufferTextWriter.Get(output);
5049
try
5150
{
52-
document.Serialize(new OpenApiJsonWriter(writer), documentOptions.OpenApiVersion);
51+
document.Serialize(new ScrubbingOpenApiJsonWriter(writer), documentOptions.OpenApiVersion);
5352
context.Response.ContentType = "application/json;charset=utf-8";
5453
await context.Response.BodyWriter.WriteAsync(output.ToArray(), context.RequestAborted);
5554
await context.Response.BodyWriter.FlushAsync(context.RequestAborted);

src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
304304
break;
305305
case OpenApiConstants.SchemaId:
306306
reader.Read();
307-
schema.Extensions.Add(OpenApiConstants.SchemaId, new OpenApiString(reader.GetString()));
307+
schema.Extensions.Add(OpenApiConstants.SchemaId, new ScrubbedOpenApiAny(reader.GetString()));
308308
break;
309309
// OpenAPI does not support the `const` keyword in its schema implementation, so
310310
// we map it to its closest approximation, an enum with a single value, here.

src/OpenApi/src/Services/OpenApiDocumentProvider.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Microsoft.Extensions.Options;
77
using Microsoft.OpenApi;
88
using Microsoft.OpenApi.Extensions;
9-
using Microsoft.OpenApi.Writers;
109
using System.Linq;
1110

1211
namespace Microsoft.Extensions.ApiDescriptions;
@@ -41,7 +40,7 @@ public async Task GenerateAsync(string documentName, TextWriter writer, OpenApiS
4140
// more info.
4241
var targetDocumentService = serviceProvider.GetRequiredKeyedService<OpenApiDocumentService>(documentName);
4342
var document = await targetDocumentService.GetOpenApiDocumentAsync();
44-
var jsonWriter = new OpenApiJsonWriter(writer);
43+
var jsonWriter = new ScrubbingOpenApiJsonWriter(writer);
4544
document.Serialize(jsonWriter, openApiSpecVersion);
4645
}
4746

src/OpenApi/src/Services/OpenApiDocumentService.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
using Microsoft.Extensions.DependencyInjection;
2121
using Microsoft.Extensions.Hosting;
2222
using Microsoft.Extensions.Options;
23-
using Microsoft.OpenApi.Any;
2423
using Microsoft.OpenApi.Models;
2524

2625
namespace Microsoft.AspNetCore.OpenApi;
@@ -34,7 +33,6 @@ internal sealed class OpenApiDocumentService(
3433
{
3534
private readonly OpenApiOptions _options = optionsMonitor.Get(documentName);
3635
private readonly OpenApiSchemaService _componentService = serviceProvider.GetRequiredKeyedService<OpenApiSchemaService>(documentName);
37-
private readonly IOpenApiDocumentTransformer _scrubExtensionsTransformer = new ScrubExtensionsTransformer();
3836
private readonly IOpenApiDocumentTransformer _schemaReferenceTransformer = new OpenApiSchemaReferenceTransformer();
3937

4038
private static readonly OpenApiEncoding _defaultFormEncoding = new OpenApiEncoding { Style = ParameterStyle.Form, Explode = true };
@@ -82,8 +80,6 @@ private async Task ApplyTransformersAsync(OpenApiDocument document, Cancellation
8280
}
8381
// Move duplicated JSON schemas to the global components.schemas object and map references after all transformers have run.
8482
await _schemaReferenceTransformer.TransformAsync(document, documentTransformerContext, cancellationToken);
85-
// Remove `x-aspnetcore-id` and `x-schema-id` extensions from operations after all transformers have run.
86-
await _scrubExtensionsTransformer.TransformAsync(document, documentTransformerContext, cancellationToken);
8783
}
8884

8985
// Note: Internal for testing.
@@ -126,7 +122,7 @@ private async Task<Dictionary<OperationType, OpenApiOperation>> GetOperationsAsy
126122
foreach (var description in descriptions)
127123
{
128124
var operation = await GetOperationAsync(description, capturedTags, cancellationToken);
129-
operation.Extensions.Add(OpenApiConstants.DescriptionId, new OpenApiString(description.ActionDescriptor.Id));
125+
operation.Extensions.Add(OpenApiConstants.DescriptionId, new ScrubbedOpenApiAny(description.ActionDescriptor.Id));
130126
_operationTransformerContextCache.TryAdd(description.ActionDescriptor.Id, new OpenApiOperationTransformerContext
131127
{
132128
DocumentName = documentName,

src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.IO.Pipelines;
55
using System.Text.Json.Nodes;
66
using Microsoft.AspNetCore.Http;
7-
using Microsoft.OpenApi.Any;
87
using Microsoft.OpenApi.Models;
98

109
namespace Microsoft.AspNetCore.OpenApi;
@@ -106,7 +105,7 @@ public void PopulateSchemaIntoReferenceCache(OpenApiSchema schema, bool captureS
106105
// ID to support disambiguating between a derived type on its own and a derived type
107106
// as part of a polymorphic schema.
108107
var baseTypeSchemaId = schema.Extensions.TryGetValue(OpenApiConstants.SchemaId, out var schemaId)
109-
? ((OpenApiString)schemaId).Value
108+
? ((ScrubbedOpenApiAny)schemaId).Value
110109
: null;
111110
foreach (var anyOfSchema in schema.AnyOf)
112111
{
@@ -178,7 +177,7 @@ private void AddOrUpdateSchemaByReference(OpenApiSchema schema, string? baseType
178177
private static string? GetSchemaReferenceId(OpenApiSchema schema)
179178
{
180179
if (schema.Extensions.TryGetValue(OpenApiConstants.SchemaId, out var referenceIdAny)
181-
&& referenceIdAny is OpenApiString { Value: string referenceId })
180+
&& referenceIdAny is ScrubbedOpenApiAny { Value: string referenceId })
182181
{
183182
return referenceId;
184183
}

src/OpenApi/src/Transformers/DelegateOpenApiDocumentTransformer.cs

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

44
using Microsoft.Extensions.DependencyInjection;
5-
using Microsoft.OpenApi.Any;
65
using Microsoft.OpenApi.Models;
76

87
namespace Microsoft.AspNetCore.OpenApi;
@@ -43,7 +42,7 @@ public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransf
4342
}
4443

4544
if (operation.Extensions.TryGetValue(OpenApiConstants.DescriptionId, out var descriptionIdExtension) &&
46-
descriptionIdExtension is OpenApiString { Value: var descriptionId } &&
45+
descriptionIdExtension is ScrubbedOpenApiAny { Value: string descriptionId } &&
4746
documentService.TryGetCachedOperationTransformerContext(descriptionId, out var operationContext))
4847
{
4948
await _operationTransformer(operation, operationContext, cancellationToken);

src/OpenApi/src/Transformers/Implementations/ScrubExtensionsTransformer.cs

Lines changed: 0 additions & 124 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.OpenApi;
5+
using Microsoft.OpenApi.Any;
6+
using Microsoft.OpenApi.Writers;
7+
8+
namespace Microsoft.AspNetCore.OpenApi;
9+
10+
/// <summary>
11+
/// Represents an <see cref="IOpenApiAny"/> instance that does not serialize itself to
12+
/// the outgoing document.
13+
///
14+
/// The no-op implementation of the <see cref="Write(IOpenApiWriter, OpenApiSpecVersion)"/> method
15+
/// prevents the value of these properties from being written to disk. When used in conjunction with
16+
/// the logic to exempt these properties from serialization in <see cref="ScrubbingOpenApiJsonWriter"/>,
17+
/// we achieve the desired result of not serializing these properties to the output document but retaining
18+
/// them in the in-memory document.
19+
/// </summary>
20+
internal sealed class ScrubbedOpenApiAny(string? value) : IOpenApiAny
21+
{
22+
public AnyType AnyType { get; } = AnyType.Primitive;
23+
24+
public string? Value { get; } = value;
25+
26+
public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion)
27+
{
28+
return;
29+
}
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.OpenApi.Writers;
5+
6+
namespace Microsoft.AspNetCore.OpenApi;
7+
8+
/// <summary>
9+
/// Represents a JSON writer that scrubs certain properties from the output,
10+
/// specifically the schema ID and description ID that are used for schema resolution
11+
/// and action descriptor resolution in the in-memory OpenAPI document.
12+
///
13+
/// In conjunction with <see cref="ScrubbedOpenApiAny" /> this allows us to work around
14+
/// the lack of an in-memory property bag on the OpenAPI object model and allows us to
15+
/// avoid having to scrub the properties in the OpenAPI document prior to serialization.
16+
///
17+
/// For more information, see https://github.com/microsoft/OpenAPI.NET/issues/1719.
18+
/// </summary>
19+
internal sealed class ScrubbingOpenApiJsonWriter(TextWriter textWriter) : OpenApiJsonWriter(textWriter)
20+
{
21+
public override void WritePropertyName(string name)
22+
{
23+
if (name == OpenApiConstants.SchemaId || name == OpenApiConstants.DescriptionId)
24+
{
25+
return;
26+
}
27+
28+
base.WritePropertyName(name);
29+
}
30+
}

src/OpenApi/test/Comparers/OpenApiAnyComparerTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ public class OpenApiAnyComparerTests
3939
[new OpenApiArray { new OpenApiString("value") }, new OpenApiArray { new OpenApiString("value") }, true],
4040
[new OpenApiArray { new OpenApiString("value") }, new OpenApiArray { new OpenApiString("value2") }, false],
4141
[new OpenApiArray { new OpenApiString("value2"), new OpenApiString("value") }, new OpenApiArray { new OpenApiString("value"), new OpenApiString("value2") }, false],
42-
[new OpenApiArray { new OpenApiString("value"), new OpenApiString("value") }, new OpenApiArray { new OpenApiString("value"), new OpenApiString("value") }, true]
42+
[new OpenApiArray { new OpenApiString("value"), new OpenApiString("value") }, new OpenApiArray { new OpenApiString("value"), new OpenApiString("value") }, true],
43+
[new ScrubbedOpenApiAny("value"), new ScrubbedOpenApiAny("value"), true],
44+
[new ScrubbedOpenApiAny("value"), new ScrubbedOpenApiAny("value2"), false],
45+
[new ScrubbedOpenApiAny(null), new ScrubbedOpenApiAny(null), true]
4346
];
4447

4548
[Theory]

src/OpenApi/test/Integration/OpenApiDocumentIntegrationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ await Verifier.Verify(GetOpenApiJson(document))
3232
private static string GetOpenApiJson(OpenApiDocument document)
3333
{
3434
using var textWriter = new StringWriter(CultureInfo.InvariantCulture);
35-
var jsonWriter = new OpenApiJsonWriter(textWriter);
35+
var jsonWriter = new ScrubbingOpenApiJsonWriter(textWriter);
3636
document.SerializeAsV3(jsonWriter);
3737
return textWriter.ToString();
3838
}

0 commit comments

Comments
 (0)