diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs index 12b0c1ed996c..931c1a80c546 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs @@ -337,7 +337,7 @@ internal static IOpenApiSchema ResolveReferenceForSchema(OpenApiDocument documen if (schema.Metadata.TryGetValue(OpenApiConstants.SchemaId, out var schemaId) && schemaId is string schemaIdString) { - return document.AddOpenApiSchemaByReference(schemaIdString, schema); + return new OpenApiSchemaReference(schemaIdString, document); } var relativeSchemaId = $"#/components/schemas/{rootSchemaId}{refIdString.Replace("#", string.Empty)}"; return new OpenApiSchemaReference(relativeSchemaId, document); diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs index 1d49c03970b5..2979e5198444 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs @@ -1004,6 +1004,124 @@ await VerifyOpenApiDocument(builder, document => }); } + // Test for: https://github.com/dotnet/aspnetcore/issues/63503 + [Fact] + public async Task HandlesCircularReferencesRegardlessOfPropertyOrder_SelfFirst() + { + var builder = CreateBuilder(); + builder.MapPost("/", (DirectCircularModelSelfFirst dto) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + Assert.NotNull(document.Components?.Schemas); + var schema = document.Components.Schemas["DirectCircularModelSelfFirst"]; + Assert.Equal(JsonSchemaType.Object, schema.Type); + Assert.NotNull(schema.Properties); + Assert.Collection(schema.Properties, + property => + { + Assert.Equal("self", property.Key); + var reference = Assert.IsType(property.Value); + Assert.Equal("#/components/schemas/DirectCircularModelSelfFirst", reference.Reference.ReferenceV3); + }, + property => + { + Assert.Equal("referenced", property.Key); + var reference = Assert.IsType(property.Value); + }); + + // Verify that it does not result in an empty schema for a referenced schema + var referencedSchema = document.Components.Schemas["ReferencedModel"]; + Assert.NotNull(referencedSchema.Properties); + Assert.NotEmpty(referencedSchema.Properties); + var idProperty = Assert.Single(referencedSchema.Properties); + Assert.Equal("id", idProperty.Key); + var idPropertySchema = Assert.IsType(idProperty.Value); + Assert.Equal(JsonSchemaType.Integer, idPropertySchema.Type); + }); + } + + // Test for: https://github.com/dotnet/aspnetcore/issues/63503 + [Fact] + public async Task HandlesCircularReferencesRegardlessOfPropertyOrder_SelfLast() + { + var builder = CreateBuilder(); + builder.MapPost("/", (DirectCircularModelSelfLast dto) => { }); + + await VerifyOpenApiDocument(builder, document => + { + Assert.NotNull(document.Components?.Schemas); + var schema = document.Components.Schemas["DirectCircularModelSelfLast"]; + Assert.Equal(JsonSchemaType.Object, schema.Type); + Assert.NotNull(schema.Properties); + Assert.Collection(schema.Properties, + property => + { + Assert.Equal("referenced", property.Key); + var reference = Assert.IsType(property.Value); + }, + property => + { + Assert.Equal("self", property.Key); + var reference = Assert.IsType(property.Value); + Assert.Equal("#/components/schemas/DirectCircularModelSelfLast", reference.Reference.ReferenceV3); + }); + + // Verify that it does not result in an empty schema for a referenced schema + var referencedSchema = document.Components.Schemas["ReferencedModel"]; + Assert.NotNull(referencedSchema.Properties); + Assert.NotEmpty(referencedSchema.Properties); + var idProperty = Assert.Single(referencedSchema.Properties); + Assert.Equal("id", idProperty.Key); + var idPropertySchema = Assert.IsType(idProperty.Value); + Assert.Equal(JsonSchemaType.Integer, idPropertySchema.Type); + }); + } + + // Test for: https://github.com/dotnet/aspnetcore/issues/63503 + [Fact] + public async Task HandlesCircularReferencesRegardlessOfPropertyOrder_MultipleSelf() + { + var builder = CreateBuilder(); + builder.MapPost("/", (DirectCircularModelMultiple dto) => { }); + + await VerifyOpenApiDocument(builder, document => + { + Assert.NotNull(document.Components?.Schemas); + var schema = document.Components.Schemas["DirectCircularModelMultiple"]; + Assert.Equal(JsonSchemaType.Object, schema.Type); + Assert.NotNull(schema.Properties); + Assert.Collection(schema.Properties, + property => + { + Assert.Equal("selfFirst", property.Key); + var reference = Assert.IsType(property.Value); + Assert.Equal("#/components/schemas/DirectCircularModelMultiple", reference.Reference.ReferenceV3); + }, + property => + { + Assert.Equal("referenced", property.Key); + var reference = Assert.IsType(property.Value); + }, + property => + { + Assert.Equal("selfLast", property.Key); + var reference = Assert.IsType(property.Value); + Assert.Equal("#/components/schemas/DirectCircularModelMultiple", reference.Reference.ReferenceV3); + }); + + // Verify that it does not result in an empty schema for a referenced schema + var referencedSchema = document.Components.Schemas["ReferencedModel"]; + Assert.NotNull(referencedSchema.Properties); + Assert.NotEmpty(referencedSchema.Properties); + var idProperty = Assert.Single(referencedSchema.Properties); + Assert.Equal("id", idProperty.Key); + var idPropertySchema = Assert.IsType(idProperty.Value); + Assert.Equal(JsonSchemaType.Integer, idPropertySchema.Type); + }); + } + // Test models for issue 61194 private class Config { @@ -1060,5 +1178,30 @@ public sealed class RefUser public string Name { get; set; } = ""; public string Email { get; set; } = ""; } + + // Test models for issue 63503 + private class DirectCircularModelSelfFirst + { + public DirectCircularModelSelfFirst Self { get; set; } = null!; + public ReferencedModel Referenced { get; set; } = null!; + } + + private class DirectCircularModelSelfLast + { + public ReferencedModel Referenced { get; set; } = null!; + public DirectCircularModelSelfLast Self { get; set; } = null!; + } + + private class DirectCircularModelMultiple + { + public DirectCircularModelMultiple SelfFirst { get; set; } = null!; + public ReferencedModel Referenced { get; set; } = null!; + public DirectCircularModelMultiple SelfLast { get; set; } = null!; + } + + private class ReferencedModel + { + public int Id { get; set; } + } } #nullable restore