Skip to content

[Critical] ASP.NET Core 9.0: AddOpenApi duplicates schema with a number suffix #59427

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
1 task done
jaliyaudagedara opened this issue Dec 11, 2024 · 10 comments
Closed
1 task done
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi

Comments

@jaliyaudagedara
Copy link

jaliyaudagedara commented Dec 11, 2024

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I was migrating an ASP.NET Core Web API from .NET 8.0 to .NET 9.0.

As part of the migration, I was replacing AddSwaggerGen() with AddOpenApi() and seeing this critical issue. This basically inserts duplicated schema to OpenAPI specification. Solid OpenAPI specification is critical when integrating with services like APIM and hence subject is marked with Critical.

Consider the following minimal reproducible example.

csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.2.0" />
  </ItemGroup>

</Project>

Startup.cs

namespace WebApplication3;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOpenApi();
        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseSwaggerUI(x =>
        {
            x.SwaggerEndpoint("/openapi/v1.json", "My API");
        });

        app.UseHttpsRedirection();
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapOpenApi();

            endpoints.MapControllers();
        });
    }
}

Program.cs

namespace WebApplication3;

public class Program
{
    public static void Main(string[] args)
    {
        IHost host = CreateHostBuilder(args).Build();
        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
       Host.CreateDefaultBuilder(args)
           .ConfigureWebHostDefaults(webBuilder =>
           {
               webBuilder.UseStartup<Startup>();
           });
}

WeatherForecastController.cs

using Microsoft.AspNetCore.Mvc;

namespace WebApplication3.Controllers;

[ApiController]
[Route("/api/[controller]")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public ActionResult<IEnumerable<WeatherForecast>> Get()
    {
        return Ok();
    }

    [HttpPost]
    public ActionResult<WeatherForecast> Create([FromBody] WeatherForecast weatherForecast)
    {
        return weatherForecast;
    }
}

Up to here, it's pretty standard.

Now carefully review the following WeatherForecast type.

namespace WebApplication3;

public class WeatherForecast
{
    public int Id { get; set; }

    public DateOnly Date { get; set; }

    public int TemperatureC { get; set; }

    public LocationDto Location { get; set; }
}

public class LocationDto
{
    public string Name { get; set; }

    public AddressDto Address { get; set; }
}

public class AddressDto
{
    string AddressLine { get; set; }

    // A circular reference, but not getting any errors
    public LocationDto RelatedLocation { get; set; }
}

When I run this:
Image

Note the highlighted schema.

OpenAPI specification

{
    "openapi": "3.0.1",
    "info": {
        "title": "WebApplication3 | v1",
        "version": "1.0.0"
    },
    "servers": [
        {
            "url": "https://localhost:7286/"
        },
        {
            "url": "http://localhost:5013/"
        }
    ],
    "paths": {
        "/api/WeatherForecast": {
            "get": {
                "tags": [
                    "WeatherForecast"
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "text/plain": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/WeatherForecast"
                                    }
                                }
                            },
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/WeatherForecast"
                                    }
                                }
                            },
                            "text/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/WeatherForecast"
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "tags": [
                    "WeatherForecast"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/WeatherForecast2"
                            }
                        },
                        "text/json": {
                            "schema": {
                                "$ref": "#/components/schemas/WeatherForecast2"
                            }
                        },
                        "application/*+json": {
                            "schema": {
                                "$ref": "#/components/schemas/WeatherForecast2"
                            }
                        }
                    },
                    "required": true
                },
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "text/plain": {
                                "schema": {
                                    "$ref": "#/components/schemas/WeatherForecast2"
                                }
                            },
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/WeatherForecast2"
                                }
                            },
                            "text/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/WeatherForecast2"
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "AddressDto": {
                "type": "object",
                "properties": {
                    "relatedLocation": {
                        "$ref": "#/components/schemas/LocationDto2"
                    }
                }
            },
            "AddressDto2": {
                "type": "object",
                "properties": {
                    "relatedLocation": {
                        "$ref": "#/components/schemas/LocationDto4"
                    }
                }
            },
            "LocationDto": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "address": {
                        "$ref": "#/components/schemas/AddressDto"
                    }
                }
            },
            "LocationDto2": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "address": {
                        "$ref": "#/components/schemas/#/items/properties/location/properties/address"
                    }
                }
            },
            "LocationDto3": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "address": {
                        "$ref": "#/components/schemas/AddressDto2"
                    }
                }
            },
            "LocationDto4": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "address": {
                        "$ref": "#/components/schemas/#/properties/location/properties/address"
                    }
                }
            },
            "WeatherForecast": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "date": {
                        "type": "string",
                        "format": "date"
                    },
                    "temperatureC": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "location": {
                        "$ref": "#/components/schemas/LocationDto"
                    }
                }
            },
            "WeatherForecast2": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "date": {
                        "type": "string",
                        "format": "date"
                    },
                    "temperatureC": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "location": {
                        "$ref": "#/components/schemas/LocationDto3"
                    }
                }
            }
        }
    },
    "tags": [
        {
            "name": "WeatherForecast"
        }
    ]
}

Expected Behavior

Under components.schemas, there should be only 3 schemas

  • AddressDto
  • LocationDto
  • WeatherForecast

Steps To Reproduce

https://github.com/jaliyaudagedara/aspnetcore-minimal-repros/tree/main/openapi-dup-schemas

Exceptions (if any)

N/A

.NET Version

9.0.200-preview.0.24575.35

Anything else?

N/A

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Dec 11, 2024
@jaliyaudagedara jaliyaudagedara changed the title [Critical] AddOpenApi duplicates schema with a number suffix [Critical] ASP.NET Core 9.0 AddOpenApi duplicates schema with a number suffix Dec 11, 2024
@jaliyaudagedara jaliyaudagedara changed the title [Critical] ASP.NET Core 9.0 AddOpenApi duplicates schema with a number suffix [Critical] ASP.NET Core 9.0: AddOpenApi duplicates schema with a number suffix Dec 11, 2024
@martincostello
Copy link
Member

Please provide the repro as a GitHub repository, not a ZIP file.

@jaliyaudagedara
Copy link
Author

sure, will do within next 30 min

@jaliyaudagedara
Copy link
Author

jaliyaudagedara commented Dec 11, 2024

@martincostello:
https://github.com/jaliyaudagedara/repro-aspnetcore-openapi-dup-schemas

Updated the original issue with the link as well.

@jaliyaudagedara
Copy link
Author

jaliyaudagedara commented Dec 11, 2024

Issue seems to be here:

private void AddOrUpdateSchemaByReference(OpenApiSchema schema, string? baseTypeSchemaId = null, bool captureSchemaByRef = false)

{
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "location": {
                "type": "object",
                "properties": {
                    "address": {
                        "type": "object",
                        "properties": {
                            "relatedLocation": {
                                "type": "object",
                                "properties": {
                                    "address": {
                                        "$ref": "#/items/properties/location/properties/address",
                                        "x-schema-id": "AddressDto"
                                    }
                                },
                                "x-schema-id": "LocationDto"
                            }
                        },
                        "x-schema-id": "AddressDto"
                    }
                },
                "x-schema-id": "LocationDto"
            }
        },
        "x-schema-id": "WeatherForecast"
    }
}

LocationDto schema is different.

{
    "type": "object",
    "properties": {
        "address": {
            "$ref": "#/items/properties/location/properties/address",
            "x-schema-id": "AddressDto"
        }
    },
    "x-schema-id": "LocationDto"
}

{
    "type": "object",
    "properties": {
        "address": {
            "type": "object",
            "properties": {
                "relatedLocation": {
                    "type": "object",
                    "properties": {
                        "address": {
                            "$ref": "#/items/properties/location/properties/address",
                            "x-schema-id": "AddressDto"
                        }
                    },
                    "x-schema-id": "LocationDto"
                }
            },
            "x-schema-id": "AddressDto"
        }
    },
    "x-schema-id": "LocationDto"
}

Should the OpenApiSchema for WeatherForecast be this,

{
    "type": "object",
    "properties": {
        "location": {
            "type": "object",
            "properties": {
                "address": {
                    "type": "object",
                    "properties": {
                        "relatedLocation": {
                            "type": "object",
                            "properties": {
                                "address": {
                                    "$ref": "#/items/properties/location/properties/address",
                                    "x-schema-id": "AddressDto"
                                }
                            },
                            // added
                            "$ref": "#/items/properties/location",
                            "x-schema-id": "LocationDto"
                        }
                    },
                    "x-schema-id": "AddressDto"
                }
            },
            "x-schema-id": "LocationDto"
        }
    },
    "x-schema-id": "WeatherForecast"
}

@jaliyaudagedara
Copy link
Author

jaliyaudagedara commented Dec 11, 2024

Updated the repro with minimal APIs,

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

WebApplication app = builder.Build();

app.MapOpenApi();
app.UseSwaggerUI(x =>
{
    x.SwaggerEndpoint("/openapi/v1.json", "My API");
});

app.UseHttpsRedirection();

app.MapGet("/weatherforecast", () =>
{
    return Enumerable.Empty<WeatherForecast>();
})
.Produces<IEnumerable<WeatherForecast>>()
.WithName("GetWeatherForecasts");

app.MapPost("/weatherforecast", (WeatherForecast weatherForecast) =>
{
    return weatherForecast;
})
.Produces<WeatherForecast>()
.WithName("CreateWeatherForecast");

app.Run();

public class WeatherForecast
{
    public LocationDto Location { get; set; }
}

public class LocationDto
{
    public AddressDto Address { get; set; }
}

public class AddressDto
{
    public LocationDto RelatedLocation { get; set; }
}

@jankaltenecker
Copy link

Duplicate issue: #58968

@akamor
Copy link

akamor commented Dec 16, 2024

@martincostello Lots of additional reproductions and information in the issue @jankaltenecker just linked. It really looks like openapi is dead on arrival in dotnet9. It cannot even support quite trivial schemas given this duplication issue.

@jaliyaudagedara
Copy link
Author

@jankaltenecker, thanks for linking the issue.

@akamor, completely agree.

@captainsafia
Copy link
Member

Closing in favor of #58968.

See update at #58968 (comment).

@jaliyaudagedara
Copy link
Author

@captainsafia, Thanks Safia!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi
Projects
None yet
Development

No branches or pull requests

6 participants