Skip to content

Commit 874aaf4

Browse files
authored
Add support for reference-based schemas in OpenAPI document (#56175)
* Add support for reference-based schemas in OpenAPI document * Use ReferenceEquals in equality checks and address feedback * Support generating friendly reference names * Add schema spec tests, move controller tests, move dedupe logic * Add more scenarios to snapsho tests * Harden OpenApi comparer implementations * Add tests for schema transformers + refs * Address more feedback
1 parent cad47d2 commit 874aaf4

File tree

41 files changed

+2517
-620
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2517
-620
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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.Globalization;
5+
using BenchmarkDotNet.Attributes;
6+
using Microsoft.OpenApi.Any;
7+
using Microsoft.OpenApi.Interfaces;
8+
using Microsoft.OpenApi.Models;
9+
10+
namespace Microsoft.AspNetCore.OpenApi.Microbenchmarks;
11+
12+
public class OpenApiSchemaComparerBenchmark
13+
{
14+
[Params(1, 10, 100)]
15+
public int ElementCount { get; set; }
16+
17+
private OpenApiSchema _schema;
18+
19+
[GlobalSetup(Target = nameof(OpenApiSchema_GetHashCode))]
20+
public void OpenApiSchema_Setup()
21+
{
22+
_schema = new OpenApiSchema
23+
{
24+
AdditionalProperties = GenerateInnerSchema(),
25+
AdditionalPropertiesAllowed = true,
26+
AllOf = Enumerable.Range(0, ElementCount).Select(_ => GenerateInnerSchema()).ToList(),
27+
AnyOf = Enumerable.Range(0, ElementCount).Select(_ => GenerateInnerSchema()).ToList(),
28+
Deprecated = true,
29+
Default = new OpenApiString("default"),
30+
Description = "description",
31+
Discriminator = new OpenApiDiscriminator(),
32+
Example = new OpenApiString("example"),
33+
ExclusiveMaximum = true,
34+
ExclusiveMinimum = true,
35+
Extensions = new Dictionary<string, IOpenApiExtension>
36+
{
37+
["key"] = new OpenApiString("value")
38+
},
39+
ExternalDocs = new OpenApiExternalDocs(),
40+
Enum = Enumerable.Range(0, ElementCount).Select(_ => (IOpenApiAny)new OpenApiString("enum")).ToList(),
41+
OneOf = Enumerable.Range(0, ElementCount).Select(_ => GenerateInnerSchema()).ToList(),
42+
};
43+
44+
static OpenApiSchema GenerateInnerSchema() => new OpenApiSchema
45+
{
46+
Properties = Enumerable.Range(0, 10).ToDictionary(i => i.ToString(CultureInfo.InvariantCulture), _ => new OpenApiSchema()),
47+
Deprecated = true,
48+
Default = new OpenApiString("default"),
49+
Description = "description",
50+
Example = new OpenApiString("example"),
51+
Extensions = new Dictionary<string, IOpenApiExtension>
52+
{
53+
["key"] = new OpenApiString("value")
54+
},
55+
};
56+
}
57+
58+
[Benchmark]
59+
public void OpenApiSchema_GetHashCode()
60+
{
61+
OpenApiSchemaComparer.Instance.GetHashCode(_schema);
62+
}
63+
}

src/OpenApi/sample/Controllers/TestController.cs

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
[ApiController]
99
[Route("[controller]")]
10+
[ApiExplorerSettings(GroupName = "controllers")]
1011
public class TestController : ControllerBase
1112
{
1213
[HttpGet]

src/OpenApi/sample/Program.cs

+25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Frozen;
5+
using System.Collections.Immutable;
6+
using System.ComponentModel;
7+
using Microsoft.AspNetCore.Http.HttpResults;
48
using Microsoft.AspNetCore.Mvc;
59
using Microsoft.OpenApi.Models;
610
using Sample.Transformers;
@@ -24,8 +28,10 @@
2428
return Task.CompletedTask;
2529
});
2630
});
31+
builder.Services.AddOpenApi("controllers");
2732
builder.Services.AddOpenApi("responses");
2833
builder.Services.AddOpenApi("forms");
34+
builder.Services.AddOpenApi("schemas-by-ref");
2935

3036
var app = builder.Build();
3137

@@ -38,6 +44,9 @@
3844
var forms = app.MapGroup("forms")
3945
.WithGroupName("forms");
4046

47+
var schemas = app.MapGroup("schemas-by-ref")
48+
.WithGroupName("schemas-by-ref");
49+
4150
if (app.Environment.IsDevelopment())
4251
{
4352
forms.DisableAntiforgery();
@@ -84,6 +93,22 @@
8493
responses.MapGet("/triangle", () => new Triangle { Color = "red", Sides = 3, Hypotenuse = 5.0 });
8594
responses.MapGet("/shape", () => new Shape { Color = "blue", Sides = 4 });
8695

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

89114
app.Run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
7+
namespace Microsoft.AspNetCore.OpenApi;
8+
9+
internal sealed class OpenApiAnyComparer : IEqualityComparer<IOpenApiAny>
10+
{
11+
public static OpenApiAnyComparer Instance { get; } = new OpenApiAnyComparer();
12+
13+
public bool Equals(IOpenApiAny? x, IOpenApiAny? y)
14+
{
15+
if (x is null && y is null)
16+
{
17+
return true;
18+
}
19+
if (x is null || y is null)
20+
{
21+
return false;
22+
}
23+
if (object.ReferenceEquals(x, y))
24+
{
25+
return true;
26+
}
27+
28+
return x.AnyType == y.AnyType &&
29+
(x switch
30+
{
31+
OpenApiNull _ => y is OpenApiNull,
32+
OpenApiArray arrayX => y is OpenApiArray arrayY && arrayX.SequenceEqual(arrayY, Instance),
33+
OpenApiObject objectX => y is OpenApiObject objectY && objectX.Keys.Count == objectY.Keys.Count && objectX.Keys.All(key => objectY.ContainsKey(key) && Equals(objectX[key], objectY[key])),
34+
OpenApiBinary binaryX => y is OpenApiBinary binaryY && binaryX.Value.SequenceEqual(binaryY.Value),
35+
OpenApiInteger integerX => y is OpenApiInteger integerY && integerX.Value == integerY.Value,
36+
OpenApiLong longX => y is OpenApiLong longY && longX.Value == longY.Value,
37+
OpenApiDouble doubleX => y is OpenApiDouble doubleY && doubleX.Value == doubleY.Value,
38+
OpenApiFloat floatX => y is OpenApiFloat floatY && floatX.Value == floatY.Value,
39+
OpenApiBoolean booleanX => y is OpenApiBoolean booleanY && booleanX.Value == booleanY.Value,
40+
OpenApiString stringX => y is OpenApiString stringY && stringX.Value == stringY.Value,
41+
OpenApiPassword passwordX => y is OpenApiPassword passwordY && passwordX.Value == passwordY.Value,
42+
OpenApiByte byteX => y is OpenApiByte byteY && byteX.Value.SequenceEqual(byteY.Value),
43+
OpenApiDate dateX => y is OpenApiDate dateY && dateX.Value == dateY.Value,
44+
OpenApiDateTime dateTimeX => y is OpenApiDateTime dateTimeY && dateTimeX.Value == dateTimeY.Value,
45+
_ => x.Equals(y)
46+
});
47+
}
48+
49+
public int GetHashCode(IOpenApiAny obj)
50+
{
51+
var hashCode = new HashCode();
52+
hashCode.Add(obj.AnyType);
53+
if (obj is IOpenApiPrimitive primitive)
54+
{
55+
hashCode.Add(primitive.PrimitiveType);
56+
}
57+
if (obj is OpenApiBinary binary)
58+
{
59+
hashCode.AddBytes(binary.Value);
60+
}
61+
if (obj is OpenApiByte bytes)
62+
{
63+
hashCode.AddBytes(bytes.Value);
64+
}
65+
hashCode.Add<object?>(obj switch
66+
{
67+
OpenApiInteger integer => integer.Value,
68+
OpenApiLong @long => @long.Value,
69+
OpenApiDouble @double => @double.Value,
70+
OpenApiFloat @float => @float.Value,
71+
OpenApiBoolean boolean => boolean.Value,
72+
OpenApiString @string => @string.Value,
73+
OpenApiPassword password => password.Value,
74+
OpenApiDate date => date.Value,
75+
OpenApiDateTime dateTime => dateTime.Value,
76+
_ => null
77+
});
78+
79+
return hashCode.ToHashCode();
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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.Models;
6+
7+
namespace Microsoft.AspNetCore.OpenApi;
8+
9+
internal sealed class OpenApiDiscriminatorComparer : IEqualityComparer<OpenApiDiscriminator>
10+
{
11+
public static OpenApiDiscriminatorComparer Instance { get; } = new OpenApiDiscriminatorComparer();
12+
13+
public bool Equals(OpenApiDiscriminator? x, OpenApiDiscriminator? y)
14+
{
15+
if (x is null && y is null)
16+
{
17+
return true;
18+
}
19+
if (x is null || y is null)
20+
{
21+
return false;
22+
}
23+
if (object.ReferenceEquals(x, y))
24+
{
25+
return true;
26+
}
27+
28+
return x.PropertyName == y.PropertyName &&
29+
x.Mapping.Count == y.Mapping.Count &&
30+
x.Mapping.Keys.All(key => y.Mapping.ContainsKey(key) && x.Mapping[key] == y.Mapping[key]);
31+
}
32+
33+
public int GetHashCode(OpenApiDiscriminator obj)
34+
{
35+
var hashCode = new HashCode();
36+
hashCode.Add(obj.PropertyName);
37+
hashCode.Add(obj.Mapping.Count);
38+
return hashCode.ToHashCode();
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.Models;
6+
7+
namespace Microsoft.AspNetCore.OpenApi;
8+
9+
internal sealed class OpenApiExternalDocsComparer : IEqualityComparer<OpenApiExternalDocs>
10+
{
11+
public static OpenApiExternalDocsComparer Instance { get; } = new OpenApiExternalDocsComparer();
12+
13+
public bool Equals(OpenApiExternalDocs? x, OpenApiExternalDocs? y)
14+
{
15+
if (x is null && y is null)
16+
{
17+
return true;
18+
}
19+
if (x is null || y is null)
20+
{
21+
return false;
22+
}
23+
if (object.ReferenceEquals(x, y))
24+
{
25+
return true;
26+
}
27+
28+
return x.Description == y.Description &&
29+
x.Url == y.Url &&
30+
x.Extensions.Count == y.Extensions.Count
31+
&& x.Extensions.Keys.All(k => y.Extensions.ContainsKey(k) && y.Extensions[k] == x.Extensions[k]);
32+
}
33+
34+
public int GetHashCode(OpenApiExternalDocs obj)
35+
{
36+
var hashCode = new HashCode();
37+
hashCode.Add(obj.Description);
38+
hashCode.Add(obj.Url);
39+
hashCode.Add(obj.Extensions.Count);
40+
return hashCode.ToHashCode();
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.Models;
5+
6+
namespace Microsoft.AspNetCore.OpenApi;
7+
8+
internal sealed class OpenApiReferenceComparer : IEqualityComparer<OpenApiReference>
9+
{
10+
public static OpenApiReferenceComparer Instance { get; } = new OpenApiReferenceComparer();
11+
12+
public bool Equals(OpenApiReference? x, OpenApiReference? y)
13+
{
14+
if (x is null && y is null)
15+
{
16+
return true;
17+
}
18+
if (x is null || y is null)
19+
{
20+
return false;
21+
}
22+
if (object.ReferenceEquals(x, y))
23+
{
24+
return true;
25+
}
26+
27+
return x.ExternalResource == y.ExternalResource &&
28+
x.HostDocument?.HashCode == y.HostDocument?.HashCode &&
29+
x.Id == y.Id &&
30+
x.Type == y.Type;
31+
}
32+
33+
public int GetHashCode(OpenApiReference obj)
34+
{
35+
var hashCode = new HashCode();
36+
hashCode.Add(obj.ExternalResource);
37+
hashCode.Add(obj.Id);
38+
if (obj.Type is not null)
39+
{
40+
hashCode.Add(obj.Type);
41+
}
42+
return hashCode.ToHashCode();
43+
}
44+
}

0 commit comments

Comments
 (0)