Skip to content

Microsoft.AspNetCore.OpenApi fails to generate document when using polymorphic types #58213

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

Open
1 task done
ptffr opened this issue Oct 2, 2024 · 20 comments
Open
1 task done
Assignees
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi

Comments

@ptffr
Copy link

ptffr commented Oct 2, 2024

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Using polymorphic types and returning both the base class and derived class in separate controller actions causes OpenApi document generation to fail.

Document generation succeeds if you disable/remove public ComponentDto GetComponent() action in the provided example repository.

Also it seems like something is wrong with the generated document (when the exception causing controller action is removed). At most I would expect two model schemas to be generated

  • ComponentDto
  • ComponentDtoSectionDto - does Microsoft.AspNetCore.OpenApi support custom names? I don't like the auto-generated one.

but it also generates a weird looking SectionDto schema which seems like a weird mix of ComponentDto & SectionDto

Expected Behavior

OpenApi document generation should not fail and should generate correct schemas when using polymorphic types.

Steps To Reproduce

https://github.com/keenjus/OpenApiStuff/tree/7d736b83d4ddf5b4208580b9d265d690f082d7b4

Launch the project and navigate to http://localhost:5052/openapi/v1.json. Should fail with "ArgumentException: An item with the same key has already been added. Key: ComponentDtoSectionDto "

Exceptions (if any)

ArgumentException: An item with the same key has already been added. Key: ComponentDtoSectionDto
    System.Collections.Generic.Dictionary<TKey, TValue>.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
    System.Collections.Generic.Dictionary<TKey, TValue>.Add(TKey key, TValue value)
    Microsoft.AspNetCore.OpenApi.OpenApiSchemaReferenceTransformer.TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.ApplyTransformersAsync(OpenApiDocument document, IServiceProvider scopedServiceProvider, CancellationToken cancellationToken)
    Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, CancellationToken cancellationToken)
    Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, CancellationToken cancellationToken)
    Microsoft.AspNetCore.Builder.OpenApiEndpointRouteBuilderExtensions+<>c__DisplayClass0_0+<<MapOpenApi>b__0>d.MoveNext()
    Microsoft.AspNetCore.Http.Generated.<GeneratedRouteBuilderExtensions_g>F56B68D2B55B5B7B373BA2E4796D897848BC0F04A969B1AF6260183E8B9E0BAF2__GeneratedRouteBuilderExtensionsCore+<>c__DisplayClass2_0+<<MapGet0>g__RequestHandler|5>d.MoveNext()
    Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
    Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
    Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

.NET Version

9.0.100-rc.1.24452.12

Anything else?

Microsoft.AspNetCore.OpenApi 9.0.0-rtm.24501.7

@ghost ghost added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Oct 2, 2024
@captainsafia
Copy link
Member

@keenjus Thanks for filing this issue, Martin! Your repro checks out for me.

I believe the hiccup here is related to the polymorphic subtype referencing the parent in the generic type argument for the collection. I'll add a test case to our suite for this and see what the fix would look like.

ComponentDtoSectionDto - does Microsoft.AspNetCore.OpenApi support custom names? I don't like the auto-generated one.

We do support customizing the names for types, but unfortunately at the moment it won't allow you to modify the name of the polymorphic type entirely (e.g. BaseType + Subtype). :/

What kind of name would you prefer to see here?

@captainsafia captainsafia self-assigned this Oct 3, 2024
@captainsafia captainsafia added this to the 9.0.0 milestone Oct 3, 2024
@ptffr
Copy link
Author

ptffr commented Oct 4, 2024

Ideally I would like derived types to use their type names, so instead of ComponentDtoSectionDto it would be just SectionDto (Swashbuckle does it this way when using the same polymorphic types).

Full control over generated names would also be nice, so the current name generation wouldn't need changing.

@captainsafia
Copy link
Member

I believe the hiccup here is related to the polymorphic subtype referencing the parent in the generic type argument for the collection. I'll add a test case to our suite for this and see what the fix would look like.

Following up on this: I'm moving this out of 9.0.0 since I need to spend a little bit more time fleshing out the fix and the window for bringing changes in .NET 9 is closing soon. This will have to come in a servicing patch for 9.0.

Full control over generated names would also be nice, so the current name generation wouldn't need changing.

Other folks have asked for this so I'll open a separate API proposal for it.

@captainsafia captainsafia modified the milestones: 9.0.0, .NET 10 Planning Oct 10, 2024
@marinasundstrom
Copy link

marinasundstrom commented Oct 24, 2024

@captainsafia How do I influence the naming of schemas based on the .NET type? To remove a "Dto" suffix. I can't seem to find that in the documentation. I've tried the known transformers but no luck.

I'm moving my 20+ services from NSwag.

An equivalent to this:

    public class CustomSchemaNameGenerator : ISchemaNameGenerator
    {
        public string Generate(Type type)
        {
            if (type.IsGenericType)
            {
                return $"{type.Name.Replace("`1", string.Empty)}Of{GenerateName(type.GetGenericArguments().First())}";
            }
            return GenerateName(type);
        }

        private static string GenerateName(Type type)
        {
            return type.Name
                .Replace("Dto", string.Empty);
            //.Replace("Command", string.Empty)
            //.Replace("Query", string.Empty);
        }
    }

@marinasundstrom
Copy link

marinasundstrom commented Oct 24, 2024

@captainsafia

I dug into the source code and found that the actual name that will be used as reference id, or key, in the completed schema is stored under Annotations with the key x-schema-id.

I can modify that like so:

        options.AddSchemaTransformer(static (schema, context, ct) =>
        {
            const string SchemaId = "x-schema-id";

            if (schema.Annotations?.TryGetValue(SchemaId, out var referenceIdObject) == true
                 && referenceIdObject is string newReferenceId)
            {
                newReferenceId = GenerateSchemaName(context.JsonTypeInfo.Type);

                schema.Annotations[SchemaId] = newReferenceId;

                Console.WriteLine(newReferenceId);
            }

            return Task.CompletedTask;
        });

The type OpenApiConstants is not public. Otherwise, I would have used OpenApiConstants.SchemaId.

I initially wondered why the doc.Component.Schema wasn't set in the Document Transformer. But found out that the last step of creating the document is this special transformer OpenApiSchemaReferenceTransformer that runs last.

Anyway, I'm starting to understand how this works. Not that different from any compiler having to keep track of references to types, members, and variables etc. 🙂

@haefele
Copy link

haefele commented Nov 24, 2024

I'm also getting the same ArgumentException: An item with the same key has already been added., which unfortunately blocks me from updating my project to .NET 9 at this point.

@huiyuanai709
Copy link

I'm also getting the same ArgumentException: An item with the same key has already been added., which unfortunately blocks me from updating my project to .NET 9 at this point.

work with Swagger, when I try to migrate to OpenApi, get same error

Swagger:

builder.Services.AddSwaggerGen(options =>
{
    options.SchemaGeneratorOptions.UseOneOfForPolymorphism = true;
    options.SchemaGeneratorOptions.SubTypesSelector = baseType =>
        baseType.GetCustomAttributes<JsonDerivedTypeAttribute>().Select(x => x.DerivedType);
    options.SelectDiscriminatorNameUsing(baseType => baseType.GetCustomAttribute<JsonPolymorphicAttribute>()?.TypeDiscriminatorPropertyName ?? "$type");
    
    Type? SearchParentRecursive<T>(Type type) where T : Attribute
    {
        if(type.BaseType is null || type == typeof(object))
        {
            return null;
        }
        
        var attribute = type.BaseType.GetCustomAttribute<T>();
        if (attribute is null)
        {
            return SearchParentRecursive<T>(type.BaseType);
        }
        
        return type.BaseType;
    }
    options.SelectDiscriminatorValueUsing(subClass => SearchParentRecursive<JsonPolymorphicAttribute>(subClass)!.GetCustomAttributes<JsonDerivedTypeAttribute>().FirstOrDefault(x => x.DerivedType == subClass)!.TypeDiscriminator?.ToString());
});

@JoachimT99
Copy link

I'm also experiencing issues generating OpenAPI documentation for polymorphic types. I want to bind a collection of a fairly complex polymorphic type from a request. It works perfectly fine when binding only a single instance. If I use IEnumerable<T>, List<T>, or Collection<T>; it fails with An item with the same key has already been added. Key: keyName

We generate the documentation at compile-time, but I don't believe it makes any difference

@VerticalVeith
Copy link

VerticalVeith commented Jan 8, 2025

Any updates on this?
I have run into this issue in two separate projects, and it's preventing us from moving to OpenAI OpenApi. The suggestions from @marinasundstrom does get it to generate a valid OpenApi document when using an "add random stuff to the model names" strategy, but that is not a feasible fix.

@AndrewZenith
Copy link

Still a problem in 9.0.1!

@MaikelOrisha
Copy link

Facing the same issue

@egil
Copy link
Contributor

egil commented Mar 3, 2025

I am facing the same issue. A workaround for me was to change my DTO types from:

public record class BasePriceChangeDto(
    IEnumerable<KiloWattPriceRange> BasePrices) : GroupChangeDto

public record class ProductRuleChangeDto(
    ProductId ProductId,
    IEnumerable<KiloWattPriceRange> PriceRanges) : GroupChangeDto;

public record struct KiloWattPriceRange(KiloWattRange KiloWattRange, Price Price);

to:

public record class BasePriceChangeDto(
    IEnumerable<KiloWattPriceRange1> BasePrices) : GroupChangeDto

public record class ProductRuleChangeDto(
    ProductId ProductId,
    IEnumerable<KiloWattPriceRange2> PriceRanges) : GroupChangeDto;

public record struct KiloWattPriceRange1(KiloWattRange KiloWattRange, Price Price);
public record struct KiloWattPriceRange2(KiloWattRange KiloWattRange, Price Price);

@captainsafia why is it that when using the same nested type (doesn't matter if it is record struct, record class, or just plain old class) I get this error? The only change is to duplicate the nested types in the enumerable and give them separate names.

I also tried converting GroupChangeDto, BasePriceChangeDto, and ProductRuleChangeDto to plain class types to avoid having GetHashCode() be in record mode.

@basvanharten
Copy link

This issue is blocking us from switching from Swashbuckle to .NET 9 (Microsoft.AspNetCore.OpenApi).

@LeDahu22
Copy link

I'm also having the same problem with Microsoft.AspNetCore.OpenApi 9.0.3

@abbottdev
Copy link

I've just re-tested this with Microsoft.AspNetCore.OpenApi 9.0.4 and the issue seems to have disappeared. Anyone else able to confirm?

@VerticalVeith
Copy link

VerticalVeith commented Apr 11, 2025

@abbottdev I just tested the 9.0.4 on a smaller project and it spit out a valid openapi doc. I am going to try it out on a larger project on Monday, but so far it looks promising.

@AndrewZenith
Copy link

I think the AddOpenAPI might have just started ignoring the default JsonOptions, so even the bits that used to generate now don't.

.Configure(o =>
{
o.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
o.SerializerOptions.MaxDepth = 500;
});

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.Text.Json.JsonReaderException: The maximum configured depth of 64 has been exceeded. Cannot read next JSON object. LineNumber: 0 | BytePositionInLine: 20767.
at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan1 bytes) at System.Text.Json.Utf8JsonReader.StartObject() at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker) at System.Text.Json.Utf8JsonReader.ReadSingleSegment() at System.Text.Json.Utf8JsonReader.Read() at System.Text.Json.JsonDocument.Parse(ReadOnlySpan1 utf8JsonSpan, JsonReaderOptions readerOptions, MetadataDb& database, StackRowStack& stack)
at System.Text.Json.JsonDocument.ParseUnrented(ReadOnlyMemory1 utf8Json, JsonReaderOptions readerOptions, JsonTokenType tokenType) at System.Text.Json.JsonDocument.ParseValue(ReadOnlyMemory1 json, JsonDocumentOptions options)
at System.Text.Json.Nodes.JsonNode.Parse(String json, Nullable`1 nodeOptions, JsonDocumentOptions documentOptions)
at Microsoft.AspNetCore.OpenApi.OpenApiSchemaService.ResolveRelativeReferences(JsonNode node, JsonNode rootNode)
at Microsoft.AspNetCore.OpenApi.OpenApiSchemaService.ResolveRelativeReferences(JsonNode node, JsonNode rootNode)

@cs-evdlinden
Copy link

Can also confirm that the 'An item with the same key has already been added' exception with polymorphic types in 9.0.4 no longer happens 🚀

@LiHaoGit
Copy link

Can also confirm that the 'An item with the same key has already been added' exception with polymorphic types in 9.0.4 no longer happens 🚀

still throw 'An item with the same key has already been added' exception with polymorphic types in 9.0.4

@dompagoj
Copy link

Still a problem for me on 9.0.5

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