Skip to content

Add support for reference-based schemas in OpenAPI document #56175

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 8 commits into from
Jun 18, 2024
Merged
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.

using System.Globalization;
using BenchmarkDotNet.Attributes;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;

namespace Microsoft.AspNetCore.OpenApi.Microbenchmarks;

public class OpenApiSchemaComparerBenchmark
{
[Params(1, 10, 100)]
public int ElementCount { get; set; }

private OpenApiSchema _schema;

[GlobalSetup(Target = nameof(OpenApiSchema_GetHashCode))]
public void OpenApiSchema_Setup()
{
_schema = new OpenApiSchema
{
AdditionalProperties = GenerateInnerSchema(),
AdditionalPropertiesAllowed = true,
AllOf = Enumerable.Range(0, ElementCount).Select(_ => GenerateInnerSchema()).ToList(),
AnyOf = Enumerable.Range(0, ElementCount).Select(_ => GenerateInnerSchema()).ToList(),
Deprecated = true,
Default = new OpenApiString("default"),
Description = "description",
Discriminator = new OpenApiDiscriminator(),
Example = new OpenApiString("example"),
ExclusiveMaximum = true,
ExclusiveMinimum = true,
Extensions = new Dictionary<string, IOpenApiExtension>
{
["key"] = new OpenApiString("value")
},
ExternalDocs = new OpenApiExternalDocs(),
Enum = Enumerable.Range(0, ElementCount).Select(_ => (IOpenApiAny)new OpenApiString("enum")).ToList(),
OneOf = Enumerable.Range(0, ElementCount).Select(_ => GenerateInnerSchema()).ToList(),
};

static OpenApiSchema GenerateInnerSchema() => new OpenApiSchema
{
Properties = Enumerable.Range(0, 10).ToDictionary(i => i.ToString(CultureInfo.InvariantCulture), _ => new OpenApiSchema()),
Deprecated = true,
Default = new OpenApiString("default"),
Description = "description",
Example = new OpenApiString("example"),
Extensions = new Dictionary<string, IOpenApiExtension>
{
["key"] = new OpenApiString("value")
},
};
}

[Benchmark]
public void OpenApiSchema_GetHashCode()
{
OpenApiSchemaComparer.Instance.GetHashCode(_schema);
}
}
1 change: 1 addition & 0 deletions src/OpenApi/sample/Controllers/TestController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

[ApiController]
[Route("[controller]")]
[ApiExplorerSettings(GroupName = "controllers")]
public class TestController : ControllerBase
{
[HttpGet]
Expand Down
25 changes: 25 additions & 0 deletions src/OpenApi/sample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Frozen;
using System.Collections.Immutable;
using System.ComponentModel;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using Sample.Transformers;
Expand All @@ -24,8 +28,10 @@
return Task.CompletedTask;
});
});
builder.Services.AddOpenApi("controllers");
builder.Services.AddOpenApi("responses");
builder.Services.AddOpenApi("forms");
builder.Services.AddOpenApi("schemas-by-ref");

var app = builder.Build();

Expand All @@ -38,6 +44,9 @@
var forms = app.MapGroup("forms")
.WithGroupName("forms");

var schemas = app.MapGroup("schemas-by-ref")
.WithGroupName("schemas-by-ref");

if (app.Environment.IsDevelopment())
{
forms.DisableAntiforgery();
Expand Down Expand Up @@ -84,6 +93,22 @@
responses.MapGet("/triangle", () => new Triangle { Color = "red", Sides = 3, Hypotenuse = 5.0 });
responses.MapGet("/shape", () => new Shape { Color = "blue", Sides = 4 });

schemas.MapGet("/typed-results", () => TypedResults.Ok(new Triangle { Color = "red", Sides = 3, Hypotenuse = 5.0 }));
schemas.MapGet("/multiple-results", Results<Ok<Triangle>, NotFound<string>> () => Random.Shared.Next(0, 2) == 0
? TypedResults.Ok(new Triangle { Color = "red", Sides = 3, Hypotenuse = 5.0 })
: TypedResults.NotFound<string>("Item not found."));
schemas.MapGet("/iresult-no-produces", () => Results.Ok(new Triangle { Color = "red", Sides = 3, Hypotenuse = 5.0 }));
schemas.MapGet("/iresult-with-produces", () => Results.Ok(new Triangle { Color = "red", Sides = 3, Hypotenuse = 5.0 }))
.Produces<Triangle>(200, "text/xml");
schemas.MapGet("/primitives", ([Description("The ID associated with the Todo item.")] int id, [Description("The number of Todos to fetch")] int size) => { });
schemas.MapGet("/product", (Product product) => TypedResults.Ok(product));
schemas.MapGet("/account", (Account account) => TypedResults.Ok(account));
schemas.MapPost("/array-of-ints", (int[] values) => values.Sum());
schemas.MapPost("/list-of-ints", (List<int> values) => values.Count);
schemas.MapPost("/ienumerable-of-ints", (IEnumerable<int> values) => values.Count());
schemas.MapGet("/dictionary-of-ints", () => new Dictionary<string, int> { { "one", 1 }, { "two", 2 } });
schemas.MapGet("/frozen-dictionary-of-ints", () => ImmutableDictionary.CreateRange(new Dictionary<string, int> { { "one", 1 }, { "two", 2 } }));

app.MapControllers();

app.Run();
Expand Down
75 changes: 75 additions & 0 deletions src/OpenApi/src/Comparers/OpenApiAnyComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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;

namespace Microsoft.AspNetCore.OpenApi;

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

Choose a reason for hiding this comment

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

I usually give such types a private constructor, but I'm guessing you considered that to be too much ceremony.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean providing a private constructor over a static Instance property or both? What's he value of the private constructor in the later case?

Copy link
Member

Choose a reason for hiding this comment

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

I usually provide both so no one fails to notice the Instance member and instantiates it themselves.


public bool Equals(IOpenApiAny? x, IOpenApiAny? 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;
}

return x.AnyType == y.AnyType &&
(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),
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),
OpenApiDate dateX => y is OpenApiDate dateY && dateX.Value == dateY.Value,
OpenApiDateTime dateTimeX => y is OpenApiDateTime dateTimeY && dateTimeX.Value == dateTimeY.Value,
_ => x.Equals(y)
});
}

public int GetHashCode(IOpenApiAny obj)
{
var hashCode = new HashCode();
hashCode.Add(obj.AnyType);
if (obj is IOpenApiPrimitive primitive)
{
hashCode.Add(primitive.PrimitiveType);
}
hashCode.Add<object?>(obj switch
{
OpenApiBinary binary => binary.Value,
OpenApiInteger integer => integer.Value,
OpenApiLong @long => @long.Value,
OpenApiDouble @double => @double.Value,
OpenApiFloat @float => @float.Value,
OpenApiBoolean boolean => boolean.Value,
OpenApiString @string => @string.Value,
OpenApiPassword password => password.Value,
OpenApiByte @byte => @byte.Value,
OpenApiDate date => date.Value,
OpenApiDateTime dateTime => dateTime.Value,
_ => null
});

return hashCode.ToHashCode();
}
}
40 changes: 40 additions & 0 deletions src/OpenApi/src/Comparers/OpenApiDiscriminatorComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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;

internal sealed class OpenApiDiscriminatorComparer : IEqualityComparer<OpenApiDiscriminator>
{
public static OpenApiDiscriminatorComparer Instance { get; } = new OpenApiDiscriminatorComparer();

public bool Equals(OpenApiDiscriminator? x, OpenApiDiscriminator? 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;
}

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]);
}

public int GetHashCode(OpenApiDiscriminator obj)
{
var hashCode = new HashCode();
hashCode.Add(obj.PropertyName);
hashCode.Add(obj.Mapping.Count);
return hashCode.ToHashCode();
}
}
42 changes: 42 additions & 0 deletions src/OpenApi/src/Comparers/OpenApiExternalDocsComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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;

internal sealed class OpenApiExternalDocsComparer : IEqualityComparer<OpenApiExternalDocs>
{
public static OpenApiExternalDocsComparer Instance { get; } = new OpenApiExternalDocsComparer();

public bool Equals(OpenApiExternalDocs? x, OpenApiExternalDocs? 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;
}

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]);
}

public int GetHashCode(OpenApiExternalDocs obj)
{
var hashCode = new HashCode();
hashCode.Add(obj.Description);
hashCode.Add(obj.Url);
hashCode.Add(obj.Extensions.Count);
return hashCode.ToHashCode();
}
}
44 changes: 44 additions & 0 deletions src/OpenApi/src/Comparers/OpenApiReferenceComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.OpenApi.Models;

namespace Microsoft.AspNetCore.OpenApi;

internal sealed class OpenApiReferenceComparer : IEqualityComparer<OpenApiReference>
{
public static OpenApiReferenceComparer Instance { get; } = new OpenApiReferenceComparer();

public bool Equals(OpenApiReference? x, OpenApiReference? 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;
}

return x.ExternalResource == y.ExternalResource &&
x.HostDocument?.HashCode == y.HostDocument?.HashCode &&
x.Id == y.Id &&
x.Type == y.Type;
}

public int GetHashCode(OpenApiReference obj)
{
var hashCode = new HashCode();
hashCode.Add(obj.ExternalResource);
hashCode.Add(obj.Id);
if (obj.Type is not null)
{
hashCode.Add(obj.Type);
}
return hashCode.ToHashCode();
}
}
Loading
Loading