Skip to content

Implement deep copy for OpenApiSchema instances #57223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class OpenApiSchemaComparerBenchmark

private OpenApiSchema _schema;

[GlobalSetup(Target = nameof(OpenApiSchema_GetHashCode))]
[GlobalSetup]
public void OpenApiSchema_Setup()
{
_schema = new OpenApiSchema
Expand Down Expand Up @@ -56,8 +56,8 @@ public void OpenApiSchema_Setup()
}

[Benchmark]
public void OpenApiSchema_GetHashCode()
{
OpenApiSchemaComparer.Instance.GetHashCode(_schema);
}
public int OpenApiSchema_GetHashCode() => OpenApiSchemaComparer.Instance.GetHashCode(_schema);

[Benchmark]
public OpenApiSchema OpenApiSchema_Clone() => OpenApiSchemaExtensions.Clone(_schema);
}
4 changes: 3 additions & 1 deletion src/OpenApi/src/Comparers/OpenApiReferenceComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ public bool Equals(OpenApiReference? x, OpenApiReference? y)
return true;
}

// We avoid comparing the HostDocument that a reference is associated with
// so that the same schema in the OpenApiSchemaStore cache and embedded in
// an OpenAPI document is considered equal.
return x.ExternalResource == y.ExternalResource &&
x.HostDocument?.HashCode == y.HostDocument?.HashCode &&
x.Id == y.Id &&
x.Type == y.Type;
}
Expand Down
69 changes: 69 additions & 0 deletions src/OpenApi/src/Extensions/OpenApiSchemaExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;

namespace Microsoft.AspNetCore.OpenApi;

internal static class OpenApiSchemaExtensions
{
/// <summary>
/// Generates a deep copy of a given <see cref="OpenApiSchema"/> instance.
/// </summary>
/// <remarks>
/// The copy constructors of the <see cref="OpenApiSchema"/> class do not perform a deep
/// copy of the instance which presents a problem whe making modifications in deeply nested
/// subschemas. This extension implements a deep copy on <see cref="OpenApiSchema" /> to guarantee
/// that modifications on cloned subschemas do not affect the original subschema.
/// /// </remarks>
/// <param name="schema">The <see cref="OpenApiSchema"/> to generate a deep copy of.</param>
public static OpenApiSchema Clone(this OpenApiSchema schema)
{
return new OpenApiSchema
{
Title = schema.Title,
Type = schema.Type,
Format = schema.Format,
Description = schema.Description,
Maximum = schema.Maximum,
ExclusiveMaximum = schema.ExclusiveMaximum,
Minimum = schema.Minimum,
ExclusiveMinimum = schema.ExclusiveMinimum,
MaxLength = schema.MaxLength,
MinLength = schema.MinLength,
Pattern = schema.Pattern,
MultipleOf = schema.MultipleOf,
Default = OpenApiAnyCloneHelper.CloneFromCopyConstructor<IOpenApiAny>(schema.Default),
ReadOnly = schema.ReadOnly,
WriteOnly = schema.WriteOnly,
AllOf = schema.AllOf != null ? new List<OpenApiSchema>(schema.AllOf.Select(s => s.Clone())) : null,
OneOf = schema.OneOf != null ? new List<OpenApiSchema>(schema.OneOf.Select(s => s.Clone())) : null,
AnyOf = schema.AnyOf != null ? new List<OpenApiSchema>(schema.AnyOf.Select(s => s.Clone())) : null,
Not = schema.Not?.Clone(),
Required = schema.Required != null ? new HashSet<string>(schema.Required) : null,
Items = schema.Items?.Clone(),
MaxItems = schema.MaxItems,
MinItems = schema.MinItems,
UniqueItems = schema.UniqueItems,
Properties = schema.Properties?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone()),
MaxProperties = schema.MaxProperties,
MinProperties = schema.MinProperties,
AdditionalPropertiesAllowed = schema.AdditionalPropertiesAllowed,
AdditionalProperties = schema.AdditionalProperties?.Clone(),
Discriminator = schema.Discriminator != null ? new(schema.Discriminator) : null,
Example = OpenApiAnyCloneHelper.CloneFromCopyConstructor<IOpenApiAny>(schema.Example),
Enum = schema.Enum != null ? new List<IOpenApiAny>(schema.Enum) : null,
Nullable = schema.Nullable,
ExternalDocs = schema.ExternalDocs != null ? new(schema.ExternalDocs) : null,
Deprecated = schema.Deprecated,
Xml = schema.Xml != null ? new(schema.Xml) : null,
Extensions = schema.Extensions != null ? new Dictionary<string, IOpenApiExtension>(schema.Extensions) : null,
UnresolvedReference = schema.UnresolvedReference,
Reference = schema.Reference != null ? new(schema.Reference) : null,
Annotations = schema.Annotations != null ? new Dictionary<string, object>(schema.Annotations) : null,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC
// the transformation process are consistent.
document.Components.Schemas.Add(
referenceId,
ResolveReferenceForSchema(new OpenApiSchema(schema), schemasByReference, isTopLevel: true));
ResolveReferenceForSchema(schema.Clone(), schemasByReference, isTopLevel: true));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,10 @@ public void ValidatePropertiesOnOpenApiSchema()
Assert.False(OpenApiSchemaComparer.Instance.Equals(originalSchema, modifiedSchema));
Assert.True(propertyNames.Remove(nameof(OpenApiSchema.Xml)));

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

Assert.Empty(propertyNames);
Expand Down
Loading
Loading