Skip to content
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

Minimal API Group with OpenAPI and Return Type of Result<TResult> creates incorrect OpenAPI document. #57876

Open
1 task done
MarkTallentire opened this issue Sep 14, 2024 · 11 comments · May be fixed by #57972
Open
1 task done
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update.

Comments

@MarkTallentire
Copy link

MarkTallentire commented Sep 14, 2024

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I have a simple API that uses the new OpenAPI document generation and Minimal APIs that utilize groups functionality.

When using the following code, i expect OpenAPI to generate a document with the available return types of Created and BadRequest.

However I get Ok, Created and BadRequest and it also produces some weird return model types

I also see the following exception in my console.

System.ArgumentException: An item with the same key has already been added. Key: OpenApiSchemaKey { Type = Microsoft.AspNetCore.Http.HttpResults.Results2[Microsoft.AspNetCore.Http.HttpResults.Created1[Api.Features.Accounts.DTOs.CreateAccountResponse],Microsoft.AspNetCore.Http.HttpResults.BadRequest1[FluentValidation.Results.ValidationResult]], ParameterInfo = }
`

using Api.Features.Accounts.DTOs;
using Api.Features.Accounts.Services;
using Api.Features.Accounts.Validators;
using Domain;
using FluentValidation.Results;
using Microsoft.AspNetCore.Http.HttpResults;

namespace Api.Features.Accounts.Endpoints
{
    public static class AccountsEndpoints
    {
        public static RouteGroupBuilder MapAccountsEndpoints(this RouteGroupBuilder group)
        {
            group.MapPost("/", CreateAccount)
                .AllowAnonymous();

            return group;
        }

        public static async Task<Results<Created<CreateAccountResponse>, BadRequest<ValidationResult>>> CreateAccount(CreateAccountRequest accountRequest, AccountService accountService)
        {
            var validator = new CreateAccountRequestValidator();
            var validationResult = await validator.ValidateAsync(accountRequest);

            if (!validationResult.IsValid)
            {
                return TypedResults.BadRequest<ValidationResult>(validationResult);
            }
            
            var account = await accountService.CreateAccount(accountRequest.Username);

            if (account == null)
            {
                validationResult.Errors.Add(new ValidationFailure(nameof(accountRequest.Username), "Username already exists"));
                return TypedResults.BadRequest<ValidationResult>(validationResult);
            }

            return TypedResults.Created<CreateAccountResponse>(new Uri("https://google.com"), new CreateAccountResponse(account.ApiKey));
        }
    }
}
builder.Services.AddOpenApi();
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}
app.MapGroup("accounts")
    .MapAccountsEndpoints();

Produces:

image

Expected Behavior

To show only Created(201) and BadRequest(400) as available return types and only ValidationResult and CreateAccountResponse as return models.

Steps To Reproduce

Use code above in a new .net9 project and run

Exceptions (if any)

System.ArgumentException: An item with the same key has already been added. Key: OpenApiSchemaKey { Type = Microsoft.AspNetCore.Http.HttpResults.Results2[Microsoft.AspNetCore.Http.HttpResults.Created1[Api.Features.Accounts.DTOs.CreateAccountResponse],Microsoft.AspNetCore.Http.HttpResults.BadRequest1[FluentValidation.Results.ValidationResult]], ParameterInfo = }
`

.NET Version

9.0 preview

Anything else?

I have also attempted to use .Produces() to explicitly define the return types but have the same issue

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Sep 14, 2024
@martincostello
Copy link
Member

Which preview of .NET 9 are you using? If not release candidate 1, try upgrading to that - there's a bunch of bug fixes since preview 7.

@MarkTallentire
Copy link
Author

MarkTallentire commented Sep 14, 2024

@martincostello thanks for the quick response. I updated to 9.0.100-rc.1.24452.12 and it does indeed fix the documentation

Do however still see an exception, although slightly different to the original one.

 Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      System.ArgumentException: An item with the same key has already been added. Key: OpenApiSchemaKey { Type = Api.Features.Accounts.DTOs.CreateAccountResponse, ParameterInfo =  }
         at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
         at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
         at Microsoft.AspNetCore.OpenApi.OpenApiSchemaStore.GetOrAdd(OpenApiSchemaKey key, Func`2 valueFactory)
         at Microsoft.AspNetCore.OpenApi.OpenApiSchemaService.GetOrCreateSchemaAsync(Type type, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, ApiParameterDescription parameterDescription, Boolean captureSchemaByRef, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetResponseAsync(ApiDescription apiDescription, Int32 statusCode, ApiResponseType apiResponseType, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetResponsesAsync(ApiDescription description, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOperationAsync(ApiDescription description, HashSet`1 capturedTags, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOperationsAsync(IGrouping`2 descriptions, HashSet`1 capturedTags, IServiceProvider scopedServiceProvider, IOpenApiOperationTransformer[] operationTransformers, IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiPathsAsync(HashSet`1 capturedTags, IServiceProvider scopedServiceProvider, IOpenApiOperationTransformer[] operationTransformers, IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.Builder.OpenApiEndpointRouteBuilderExtensions.<>c__DisplayClass0_0.<<MapOpenApi>b__0>d.MoveNext()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Http.Generated.<GeneratedRouteBuilderExtensions_g>F56B68D2B55B5B7B373BA2E4796D897848BC0F04A969B1AF6260183E8B9E0BAF2__GeneratedRouteBuilderExtensionsCore.<>c__DisplayClass2_0.<<MapGet0>g__RequestHandler|5>d.MoveNext()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)


image

@captainsafia
Copy link
Member

@MarkTallentire Thanks for filling this issue! I'm glad @martincostello's guidance about updating to RC1 solved your first issue.

As for the second one, does the issue only happen when you access the Scalar UI? Does something similar happen when you directly navigate to the /openapi/v1.json (or whatever URL you've configured) for your endpoint on a freshly spun up server?

@captainsafia captainsafia added Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Sep 15, 2024
@MarkTallentire
Copy link
Author

@captainsafia I can confirm this only happens when I access the scalar UI, possibly a bug on their side instead of in .net?

@dotnet-policy-service dotnet-policy-service bot added Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. and removed Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. labels Sep 15, 2024
@martincostello
Copy link
Member

It might be that your second issue was fixed by #57852.

@captainsafia
Copy link
Member

It might be that your second issue was fixed by #57852.

I don't think anything in that PR would've resolved this particularly issue but I suppose it is worth a try. You can try it out by using package version 9.0.0-rc.2.24466.8 from the nightly dotnet9 package feed. You can find directions on how to access the feed in this section of the docs.

@xC0dex
Copy link

xC0dex commented Sep 17, 2024

Hey @MarkTallentire,
I reviewed the source code of the Scalar.AspNetCore package. However, I was unable to find any code paths that could cause this error. The library doesn't manipulate the generated document. Only the openapi/v1.json path is referenced.

@MarkTallentire
Copy link
Author

MarkTallentire commented Sep 17, 2024

@xC0dex Thanks for checking. @captainsafia @martincostello I updated to latest version starting with 9 available in the nightly nuget provided above but unfortunately still see the error

I've created a repo that reproduces this issue in the hope it helps diagnose. I get the error 8/10 times when running directly from a terminal using dotnet run and loading scalar/v1 in my browser.

The only changes made from the default webapi template built using dotnet new webapi are:
Install Scalar.AspNetCore and add app.MapScalarApiReference() to Program.cs
Upgrade Microsoft.AspNetCore.OpenApi to 9.0.0-rtm.24466.12
Update launchsettings.json to directly launch scalar/v1 instead of weatherforecast

Github Repo

Thanks again

@xC0dex
Copy link

xC0dex commented Sep 18, 2024

Hey @MarkTallentire,

Thanks for sharing the link to the repo. I attempted to reproduce the issue, but was unsuccessful😕.
However, I noticed that the openapi/v1.json endpoint is requested twice. This might be the cause of the problem.
image

Edit:
I was able to consistently reproduce the issue. The parallel requests are the cause. Running this code will trigger the exception almost every time the app starts:

app.Start();
var factory = app.Services.GetRequiredService<IHttpClientFactory>();
var httpClient = factory.CreateClient();
var tasks = new List<Task<HttpResponseMessage>>
{
    httpClient.GetAsync("https://localhost:7216/openapi/v1.json"),
    httpClient.GetAsync("https://localhost:7216/openapi/v1.json"),
    httpClient.GetAsync("https://localhost:7216/openapi/v1.json")
};
await Task.WhenAll(tasks);
app.WaitForShutdown();

@captainsafia
Copy link
Member

I think a regression was introduced here when we switched from ConcurrentDictionary to Dictionary in the OpenApiSchemaStore. Making an update in this file should do the trick.

@xC0dex Any interest in opening a PR for this?

@xC0dex
Copy link

xC0dex commented Sep 19, 2024

@captainsafia, sure! I just have to read through the BuildFromSource.md because I never contributed to ASP.NET Core before.

@xC0dex xC0dex linked a pull request Sep 19, 2024 that will close this issue
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants