Skip to content

Remove LINQ usage from OpenAPI comparers #56599

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

Closed
wants to merge 1 commit into from
Closed
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
63 changes: 63 additions & 0 deletions src/OpenApi/src/Comparers/ComparerHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.OpenApi;

internal static class ComparerHelpers
{
internal static bool DictionaryEquals<TKey, TValue>(IDictionary<TKey, TValue> x, IDictionary<TKey, TValue> y, IEqualityComparer<TValue> comparer)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the IDictionary and IList properties on OpenApiSchema just Dictionary and List? If so, there might be some perf benefits of adding a path that casts to the concrete types and duplicates the comparison logic.

That would avoid interface dispatch overhead, at the cost of some duplication.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the IDictionary and IList properties on OpenApiSchema just Dictionary and List?

They are. It's technically feasible for an end-user to override them with their own implementations but I suspect that is not likely. This was a good idea. There's a reasonable delta between the baseline and this change.

Remove LINQ from comparers

Method TransformerCount Mean Error StdDev Op/s Gen0 Allocated
SchemaTransformer 10 28.16 us 0.510 us 0.477 us 35,515.0 0.2441 31.78 KB
SchemaTransformer 100 30.16 us 0.480 us 0.449 us 33,158.4 0.2441 31.78 KB
SchemaTransformer 1000 35.36 us 0.395 us 0.370 us 28,277.9 0.2441 31.78 KB

Cast to concrete types in overloads

Method TransformerCount Mean Error StdDev Op/s Gen0 Allocated
SchemaTransformer 10 24.39 us 0.486 us 0.431 us 40,995.4 0.2441 31.19 KB
SchemaTransformer 100 25.32 us 0.304 us 0.284 us 39,491.7 0.2441 31.19 KB
SchemaTransformer 1000 29.84 us 0.328 us 0.290 us 33,517.4 0.2441 32.19 KB

where TKey : notnull
where TValue : notnull
{
if (x.Keys.Count != y.Keys.Count)
{
return false;
}

foreach (var key in x.Keys)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to A/B test as I don't know the answer.

Would iterating like foreach ((var key, var valueX) in x) and avoiding the x[key] call be better or worse? I guess it depends on if iterating the keys is cheaper that iterating both and how often the right hand side of the || is accessed.

{
if (!y.TryGetValue(key, out var value) || !comparer.Equals(x[key], value))
{
return false;
}
}

return true;
}

internal static bool ListEquals<T>(IList<T> x, IList<T> y, IEqualityComparer<T> comparer)
{
if (x.Count != y.Count)
{
return false;
}

for (var i = 0; i < x.Count; i++)
{
if (!comparer.Equals(x[i], y[i]))
{
return false;
}
}

return true;
}

internal static bool ByteArrayEquals(byte[] x, byte[] y)
{
if (x.Length != y.Length)
{
return false;
}

for (var i = 0; i < x.Length; i++)
{
if (!Equals(x[i], y[i]))
{
return false;
}
}
Comment on lines +53 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would SequenceEquals() on the two arrays be more performant maybe?


return true;
}
}
47 changes: 41 additions & 6 deletions src/OpenApi/src/Comparers/OpenApiAnyComparer.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// 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;

namespace Microsoft.AspNetCore.OpenApi;

internal sealed class OpenApiAnyComparer : IEqualityComparer<IOpenApiAny>
internal sealed class OpenApiAnyComparer : IEqualityComparer<IOpenApiAny>, IEqualityComparer<IOpenApiExtension>
{
public static OpenApiAnyComparer Instance { get; } = new OpenApiAnyComparer();

Expand All @@ -29,23 +29,48 @@ public bool Equals(IOpenApiAny? x, IOpenApiAny? y)
(x switch
{
OpenApiNull _ => y is OpenApiNull,
OpenApiArray arrayX => y is OpenApiArray arrayY && arrayX.SequenceEqual(arrayY, Instance),
OpenApiObject objectX => y is OpenApiObject objectY && objectX.Keys.Count == objectY.Keys.Count && objectX.Keys.All(key => objectY.ContainsKey(key) && Equals(objectX[key], objectY[key])),
OpenApiBinary binaryX => y is OpenApiBinary binaryY && binaryX.Value.SequenceEqual(binaryY.Value),
OpenApiArray arrayX => y is OpenApiArray arrayY && ComparerHelpers.ListEquals(arrayX, arrayY, Instance),
OpenApiObject objectX => y is OpenApiObject objectY && ComparerHelpers.DictionaryEquals(objectX, objectY, Instance),
OpenApiBinary binaryX => y is OpenApiBinary binaryY && ComparerHelpers.ByteArrayEquals(binaryX.Value, binaryY.Value),
OpenApiInteger integerX => y is OpenApiInteger integerY && integerX.Value == integerY.Value,
OpenApiLong longX => y is OpenApiLong longY && longX.Value == longY.Value,
OpenApiDouble doubleX => y is OpenApiDouble doubleY && doubleX.Value == doubleY.Value,
OpenApiFloat floatX => y is OpenApiFloat floatY && floatX.Value == floatY.Value,
OpenApiBoolean booleanX => y is OpenApiBoolean booleanY && booleanX.Value == booleanY.Value,
OpenApiString stringX => y is OpenApiString stringY && stringX.Value == stringY.Value,
OpenApiPassword passwordX => y is OpenApiPassword passwordY && passwordX.Value == passwordY.Value,
OpenApiByte byteX => y is OpenApiByte byteY && byteX.Value.SequenceEqual(byteY.Value),
OpenApiByte byteX => y is OpenApiByte byteY && ComparerHelpers.ByteArrayEquals(byteX.Value, byteY.Value),
OpenApiDate dateX => y is OpenApiDate dateY && dateX.Value == dateY.Value,
OpenApiDateTime dateTimeX => y is OpenApiDateTime dateTimeY && dateTimeX.Value == dateTimeY.Value,
_ => x.Equals(y)
});
}

public bool Equals(IOpenApiExtension? x, IOpenApiExtension? y)
{
if (x is null && y is null)
{
return true;
}

if (x is null || y is null)
{
return false;
}

if (object.ReferenceEquals(x, y))
{
return true;
}

if (x is IOpenApiAny openApiAnyX && y is IOpenApiAny openApiAnyY)
{
return Equals(openApiAnyX, openApiAnyY);
}

return false;
}

public int GetHashCode(IOpenApiAny obj)
{
var hashCode = new HashCode();
Expand Down Expand Up @@ -78,4 +103,14 @@ public int GetHashCode(IOpenApiAny obj)

return hashCode.ToHashCode();
}

public int GetHashCode(IOpenApiExtension obj)
{
if (obj is IOpenApiAny any)
{
return GetHashCode(any);
}

return obj.GetHashCode();
}
}
3 changes: 1 addition & 2 deletions src/OpenApi/src/Comparers/OpenApiDiscriminatorComparer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 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.Models;

namespace Microsoft.AspNetCore.OpenApi;
Expand All @@ -27,7 +26,7 @@ public bool Equals(OpenApiDiscriminator? x, OpenApiDiscriminator? y)

return x.PropertyName == y.PropertyName &&
x.Mapping.Count == y.Mapping.Count &&
x.Mapping.Keys.All(key => y.Mapping.ContainsKey(key) && x.Mapping[key] == y.Mapping[key]);
ComparerHelpers.DictionaryEquals(x.Mapping, y.Mapping, StringComparer.Ordinal);
}

public int GetHashCode(OpenApiDiscriminator obj)
Expand Down
5 changes: 2 additions & 3 deletions src/OpenApi/src/Comparers/OpenApiExternalDocsComparer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 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.Models;

namespace Microsoft.AspNetCore.OpenApi;
Expand All @@ -27,8 +26,8 @@ public bool Equals(OpenApiExternalDocs? x, OpenApiExternalDocs? y)

return x.Description == y.Description &&
x.Url == y.Url &&
x.Extensions.Count == y.Extensions.Count
&& x.Extensions.Keys.All(k => y.Extensions.ContainsKey(k) && y.Extensions[k] == x.Extensions[k]);
x.Extensions.Count == y.Extensions.Count &&
ComparerHelpers.DictionaryEquals(x.Extensions, y.Extensions, OpenApiAnyComparer.Instance);
}

public int GetHashCode(OpenApiExternalDocs obj)
Expand Down
28 changes: 18 additions & 10 deletions src/OpenApi/src/Comparers/OpenApiSchemaComparer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// 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.Models;

namespace Microsoft.AspNetCore.OpenApi;
Expand All @@ -28,19 +26,19 @@ public bool Equals(OpenApiSchema? x, OpenApiSchema? y)

return Instance.Equals(x.AdditionalProperties, y.AdditionalProperties) &&
x.AdditionalPropertiesAllowed == y.AdditionalPropertiesAllowed &&
x.AllOf.SequenceEqual(y.AllOf, Instance) &&
x.AnyOf.SequenceEqual(y.AnyOf, Instance) &&
ComparerHelpers.ListEquals(x.AllOf, y.AllOf, Instance) &&
ComparerHelpers.ListEquals(x.AnyOf, y.AnyOf, Instance) &&
x.Deprecated == y.Deprecated &&
OpenApiAnyComparer.Instance.Equals(x.Default, y.Default) &&
x.Description == y.Description &&
OpenApiDiscriminatorComparer.Instance.Equals(x.Discriminator, y.Discriminator) &&
OpenApiAnyComparer.Instance.Equals(x.Example, y.Example) &&
x.ExclusiveMaximum == y.ExclusiveMaximum &&
x.ExclusiveMinimum == y.ExclusiveMinimum &&
x.Extensions.Count == y.Extensions.Count
&& x.Extensions.Keys.All(k => y.Extensions.ContainsKey(k) && x.Extensions[k] is IOpenApiAny anyX && y.Extensions[k] is IOpenApiAny anyY && OpenApiAnyComparer.Instance.Equals(anyX, anyY)) &&
x.Extensions.Count == y.Extensions.Count &&
ComparerHelpers.DictionaryEquals(x.Extensions, y.Extensions, OpenApiAnyComparer.Instance) &&
OpenApiExternalDocsComparer.Instance.Equals(x.ExternalDocs, y.ExternalDocs) &&
x.Enum.SequenceEqual(y.Enum, OpenApiAnyComparer.Instance) &&
ComparerHelpers.ListEquals(x.Enum, y.Enum, OpenApiAnyComparer.Instance) &&
x.Format == y.Format &&
Instance.Equals(x.Items, y.Items) &&
x.Title == y.Title &&
Expand All @@ -54,20 +52,30 @@ public bool Equals(OpenApiSchema? x, OpenApiSchema? y)
x.MinLength == y.MinLength &&
x.MinProperties == y.MinProperties &&
x.MultipleOf == y.MultipleOf &&
x.OneOf.SequenceEqual(y.OneOf, Instance) &&
ComparerHelpers.ListEquals(x.OneOf, y.OneOf, Instance) &&
Instance.Equals(x.Not, y.Not) &&
x.Nullable == y.Nullable &&
x.Pattern == y.Pattern &&
x.Properties.Keys.All(k => y.Properties.ContainsKey(k) && Instance.Equals(x.Properties[k], y.Properties[k])) &&
ComparerHelpers.DictionaryEquals(x.Properties, y.Properties, Instance) &&
x.ReadOnly == y.ReadOnly &&
x.Required.Order().SequenceEqual(y.Required.Order()) &&
RequiredEquals(x.Required, y.Required) &&
OpenApiReferenceComparer.Instance.Equals(x.Reference, y.Reference) &&
x.UniqueItems == y.UniqueItems &&
x.UnresolvedReference == y.UnresolvedReference &&
x.WriteOnly == y.WriteOnly &&
OpenApiXmlComparer.Instance.Equals(x.Xml, y.Xml);
}

internal static bool RequiredEquals(ISet<string> x, ISet<string> y)
{
if (x.Count != y.Count)
{
return false;
}

return x.SetEquals(y);
}

public int GetHashCode(OpenApiSchema obj)
{
var hashCode = new HashCode();
Expand Down
5 changes: 2 additions & 3 deletions src/OpenApi/src/Comparers/OpenApiXmlComparer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 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.Models;

namespace Microsoft.AspNetCore.OpenApi;
Expand Down Expand Up @@ -30,8 +29,8 @@ public bool Equals(OpenApiXml? x, OpenApiXml? y)
x.Prefix == y.Prefix &&
x.Attribute == y.Attribute &&
x.Wrapped == y.Wrapped &&
x.Extensions.Count == y.Extensions.Count
&& x.Extensions.Keys.All(k => y.Extensions.ContainsKey(k) && y.Extensions[k] == x.Extensions[k]);
x.Extensions.Count == y.Extensions.Count &&
ComparerHelpers.DictionaryEquals(x.Extensions, y.Extensions, OpenApiAnyComparer.Instance);
}

public int GetHashCode(OpenApiXml obj)
Expand Down
Loading