Skip to content

Commit e31445e

Browse files
authored
Implement deep copy for OpenApiSchema instances (#57223)
* Implement deep copy for OpenApiSchema instances * Add more tests, use new List(), and fix reference comparer * Add comment and clean up null check * Update benchmark method return values
1 parent ad9ddd8 commit e31445e

File tree

7 files changed

+478
-8
lines changed

7 files changed

+478
-8
lines changed

src/OpenApi/perf/Microbenchmarks/OpenApiSchemaComparerBenchmark.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class OpenApiSchemaComparerBenchmark
1616

1717
private OpenApiSchema _schema;
1818

19-
[GlobalSetup(Target = nameof(OpenApiSchema_GetHashCode))]
19+
[GlobalSetup]
2020
public void OpenApiSchema_Setup()
2121
{
2222
_schema = new OpenApiSchema
@@ -56,8 +56,8 @@ public void OpenApiSchema_Setup()
5656
}
5757

5858
[Benchmark]
59-
public void OpenApiSchema_GetHashCode()
60-
{
61-
OpenApiSchemaComparer.Instance.GetHashCode(_schema);
62-
}
59+
public int OpenApiSchema_GetHashCode() => OpenApiSchemaComparer.Instance.GetHashCode(_schema);
60+
61+
[Benchmark]
62+
public OpenApiSchema OpenApiSchema_Clone() => OpenApiSchemaExtensions.Clone(_schema);
6363
}

src/OpenApi/src/Comparers/OpenApiReferenceComparer.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ public bool Equals(OpenApiReference? x, OpenApiReference? y)
2424
return true;
2525
}
2626

27+
// We avoid comparing the HostDocument that a reference is associated with
28+
// so that the same schema in the OpenApiSchemaStore cache and embedded in
29+
// an OpenAPI document is considered equal.
2730
return x.ExternalResource == y.ExternalResource &&
28-
x.HostDocument?.HashCode == y.HostDocument?.HashCode &&
2931
x.Id == y.Id &&
3032
x.Type == y.Type;
3133
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 System.Linq;
5+
using Microsoft.OpenApi.Any;
6+
using Microsoft.OpenApi.Interfaces;
7+
using Microsoft.OpenApi.Models;
8+
9+
namespace Microsoft.AspNetCore.OpenApi;
10+
11+
internal static class OpenApiSchemaExtensions
12+
{
13+
/// <summary>
14+
/// Generates a deep copy of a given <see cref="OpenApiSchema"/> instance.
15+
/// </summary>
16+
/// <remarks>
17+
/// The copy constructors of the <see cref="OpenApiSchema"/> class do not perform a deep
18+
/// copy of the instance which presents a problem whe making modifications in deeply nested
19+
/// subschemas. This extension implements a deep copy on <see cref="OpenApiSchema" /> to guarantee
20+
/// that modifications on cloned subschemas do not affect the original subschema.
21+
/// /// </remarks>
22+
/// <param name="schema">The <see cref="OpenApiSchema"/> to generate a deep copy of.</param>
23+
public static OpenApiSchema Clone(this OpenApiSchema schema)
24+
{
25+
return new OpenApiSchema
26+
{
27+
Title = schema.Title,
28+
Type = schema.Type,
29+
Format = schema.Format,
30+
Description = schema.Description,
31+
Maximum = schema.Maximum,
32+
ExclusiveMaximum = schema.ExclusiveMaximum,
33+
Minimum = schema.Minimum,
34+
ExclusiveMinimum = schema.ExclusiveMinimum,
35+
MaxLength = schema.MaxLength,
36+
MinLength = schema.MinLength,
37+
Pattern = schema.Pattern,
38+
MultipleOf = schema.MultipleOf,
39+
Default = OpenApiAnyCloneHelper.CloneFromCopyConstructor<IOpenApiAny>(schema.Default),
40+
ReadOnly = schema.ReadOnly,
41+
WriteOnly = schema.WriteOnly,
42+
AllOf = schema.AllOf != null ? new List<OpenApiSchema>(schema.AllOf.Select(s => s.Clone())) : null,
43+
OneOf = schema.OneOf != null ? new List<OpenApiSchema>(schema.OneOf.Select(s => s.Clone())) : null,
44+
AnyOf = schema.AnyOf != null ? new List<OpenApiSchema>(schema.AnyOf.Select(s => s.Clone())) : null,
45+
Not = schema.Not?.Clone(),
46+
Required = schema.Required != null ? new HashSet<string>(schema.Required) : null,
47+
Items = schema.Items?.Clone(),
48+
MaxItems = schema.MaxItems,
49+
MinItems = schema.MinItems,
50+
UniqueItems = schema.UniqueItems,
51+
Properties = schema.Properties?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone()),
52+
MaxProperties = schema.MaxProperties,
53+
MinProperties = schema.MinProperties,
54+
AdditionalPropertiesAllowed = schema.AdditionalPropertiesAllowed,
55+
AdditionalProperties = schema.AdditionalProperties?.Clone(),
56+
Discriminator = schema.Discriminator != null ? new(schema.Discriminator) : null,
57+
Example = OpenApiAnyCloneHelper.CloneFromCopyConstructor<IOpenApiAny>(schema.Example),
58+
Enum = schema.Enum != null ? new List<IOpenApiAny>(schema.Enum) : null,
59+
Nullable = schema.Nullable,
60+
ExternalDocs = schema.ExternalDocs != null ? new(schema.ExternalDocs) : null,
61+
Deprecated = schema.Deprecated,
62+
Xml = schema.Xml != null ? new(schema.Xml) : null,
63+
Extensions = schema.Extensions != null ? new Dictionary<string, IOpenApiExtension>(schema.Extensions) : null,
64+
UnresolvedReference = schema.UnresolvedReference,
65+
Reference = schema.Reference != null ? new(schema.Reference) : null,
66+
Annotations = schema.Annotations != null ? new Dictionary<string, object>(schema.Annotations) : null,
67+
};
68+
}
69+
}

src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC
3232
// the transformation process are consistent.
3333
document.Components.Schemas.Add(
3434
referenceId,
35-
ResolveReferenceForSchema(new OpenApiSchema(schema), schemasByReference, isTopLevel: true));
35+
ResolveReferenceForSchema(schema.Clone(), schemasByReference, isTopLevel: true));
3636
}
3737
}
3838

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Comparers/OpenApiSchemaComparerTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,10 @@ public void ValidatePropertiesOnOpenApiSchema()
303303
Assert.False(OpenApiSchemaComparer.Instance.Equals(originalSchema, modifiedSchema));
304304
Assert.True(propertyNames.Remove(nameof(OpenApiSchema.Xml)));
305305

306+
// Disregard annotations in comparison checks since they are in-memory constructs
306307
modifiedSchema = new(originalSchema);
307308
modifiedSchema.Annotations["key"] = "another value";
308-
Assert.False(OpenApiSchemaComparer.Instance.Equals(originalSchema, modifiedSchema));
309+
Assert.True(OpenApiSchemaComparer.Instance.Equals(originalSchema, modifiedSchema));
309310
Assert.True(propertyNames.Remove(nameof(OpenApiSchema.Annotations)));
310311

311312
Assert.Empty(propertyNames);

0 commit comments

Comments
 (0)