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

Using URL segment versioning breaks Swagger generation #1096

Open
1 task done
rdelmont opened this issue Jun 17, 2024 · 3 comments
Open
1 task done

Using URL segment versioning breaks Swagger generation #1096

rdelmont opened this issue Jun 17, 2024 · 3 comments
Assignees

Comments

@rdelmont
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I'm trying to use the url segment versioning in my project.
I changed the ODataOpenApiExample by adding
options.ApiVersionReader = new UrlSegmentApiVersionReader();
and changing the route Component for
.AddOData(options => options.AddRouteComponents("api/v{version:apiVersion}"))

Seems Ok for V1 but got this exception when trying to see the OpenAPI for V2:

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1] An unhandled exception has occurred while executing the request. Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Failed to generate Operation for action - ApiVersioning.Examples.V2.PeopleController.NewHires (ODataOpenApiExample). See inner exception ---> Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Failed to generate schema for type - Asp.Versioning.OData.ODataValue1[System.Collections.Generic.IEnumerable1[ApiVersioning.Examples.Models.Person]]. See inner exception ---> System.InvalidOperationException: Can't use schemaId "$PersonIEnumerableODataValue" for type "$Asp.Versioning.OData.ODataValue1[System.Collections.Generic.IEnumerable1[ApiVersioning.Examples.Models.Person]]". The same schemaId is already used for type "$Asp.Versioning.OData.ODataValue1[System.Collections.Generic.IEnumerable1[ApiVersioning.Examples.Models.Person]]" at Swashbuckle.AspNetCore.SwaggerGen.SchemaRepository.RegisterType(Type type, String schemaId) at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.GenerateReferencedSchema(DataContract dataContract, SchemaRepository schemaRepository, Func1 definitionFactory)
at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.GenerateConcreteSchema(DataContract dataContract, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.GenerateSchema(Type modelType, SchemaRepository schemaRepository, MemberInfo memberInfo, ParameterInfo parameterInfo, ApiParameterRouteInfo routeInfo)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateSchema(Type type, SchemaRepository schemaRepository, PropertyInfo propertyInfo, ParameterInfo parameterInfo, ApiParameterRouteInfo routeInfo)
--- End of inner exception stack trace ---
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateSchema(Type type, SchemaRepository schemaRepository, PropertyInfo propertyInfo, ParameterInfo parameterInfo, ApiParameterRouteInfo routeInfo)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.CreateResponseMediaType(ModelMetadata modelMetadata, SchemaRepository schemaRespository)
at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable1 source, Func2 keySelector, Func2 elementSelector, IEqualityComparer1 comparer)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateResponse(ApiDescription apiDescription, SchemaRepository schemaRepository, String statusCode, ApiResponseType apiResponseType)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateResponses(ApiDescription apiDescription, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository)
--- End of inner exception stack trace ---
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperations(IEnumerable1 apiDescriptions, SchemaRepository schemaRepository) at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GeneratePaths(IEnumerable1 apiDescriptions, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwaggerDocumentWithoutFilters(String documentName, String host, String basePath)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwaggerAsync(String documentName, String host, String basePath)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.OData.Routing.ODataRouteDebugMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)`

The endpoints are working fine.

Expected Behavior

I'd like to have access to the V2 and V3 of OpenAPI.

Steps To Reproduce

This is the modified code of Program.cs in ODataOpenApiExample.

`using ApiVersioning.Examples;
using Asp.Versioning;
using Asp.Versioning.Conventions;
using Microsoft.AspNetCore.OData;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;
using static Microsoft.AspNetCore.OData.Query.AllowedQueryOptions;
using PeopleControllerV2 = ApiVersioning.Examples.V2.PeopleController;
using PeopleControllerV3 = ApiVersioning.Examples.V3.PeopleController;

var builder = WebApplication.CreateBuilder( args );

// Add services to the container.

builder.Services.AddControllers()
.AddOData(
options =>
{
options.Count().Select().OrderBy();
options.RouteOptions.EnableKeyInParenthesis = false;
options.RouteOptions.EnableNonParenthesisForEmptyParameterFunction = true;
options.RouteOptions.EnablePropertyNameCaseInsensitive = true;
options.RouteOptions.EnableQualifiedOperationCall = false;
options.RouteOptions.EnableUnqualifiedOperationCall = true;
} );
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning(
options =>
{
// reporting api versions will return the headers
// "api-supported-versions" and "api-deprecated-versions"
options.ReportApiVersions = true;

                    options.Policies.Sunset( 0.9 )
                                    .Effective( DateTimeOffset.Now.AddDays( 60 ) )
                                    .Link( "policy.html" )
                                        .Title( "Versioning Policy" )
                                        .Type( "text/html" );
			        options.ApiVersionReader = new UrlSegmentApiVersionReader();
                } )
			.AddOData(options => options.AddRouteComponents("api/v{version:apiVersion}"))
            .AddODataApiExplorer(
                options =>
                {
                    // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
                    // note: the specified format code will format the version as "'v'major[.minor][-status]"
                    options.GroupNameFormat = "'v'VVV";

                    // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
                    // can also be used to control the format of the API version in route templates
                    options.SubstituteApiVersionInUrl = true;

                    // configure query options (which cannot otherwise be configured by OData conventions)
                    options.QueryOptions.Controller<PeopleControllerV2>()
                                        .Action( c => c.Get( default ) )
                                            .Allow( Skip | Count )
                                            .AllowTop( 100 )
                                            .AllowOrderBy( "firstName", "lastName" );

                    options.QueryOptions.Controller<PeopleControllerV3>()
                                        .Action( c => c.Get( default ) )
                                            .Allow( Skip | Count )
                                            .AllowTop( 100 )
                                            .AllowOrderBy( "firstName", "lastName" );
                } );

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddTransient<IConfigureOptions, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen(
options =>
{
// add a custom operation filter which sets default values
options.OperationFilter();

    var fileName = typeof( Program ).Assembly.GetName().Name + ".xml";
    var filePath = Path.Combine( AppContext.BaseDirectory, fileName );

    // integrate xml comments
    options.IncludeXmlComments( filePath );
} );

var app = builder.Build();

// Configure the HTTP request pipeline.

if ( app.Environment.IsDevelopment() )
{
// navigate to ~/$odata to determine whether any endpoints did not match an odata route template
app.UseODataRouteDebug();
}

app.UseSwagger();
app.UseSwaggerUI(
options =>
{
var descriptions = app.DescribeApiVersions();

    // build a swagger endpoint for each discovered API version
    foreach ( var description in descriptions )
    {
        var url = $"/swagger/{description.GroupName}/swagger.json";
        var name = description.GroupName.ToUpperInvariant();
        options.SwaggerEndpoint( url, name );
    }
} );

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
`

Exceptions (if any)

No response

.NET Version

8.0.204

Anything else?

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Failed to generate Operation for action - ApiVersioning.Examples.V2.PeopleController.NewHires (ODataOpenApiExample). See inner exception
---> Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Failed to generate schema for type - Asp.Versioning.OData.ODataValue1[System.Collections.Generic.IEnumerable1[ApiVersioning.Examples.Models.Person]]. See inner exception
---> System.InvalidOperationException: Can't use schemaId "$PersonIEnumerableODataValue" for type "$Asp.Versioning.OData.ODataValue1[System.Collections.Generic.IEnumerable1[ApiVersioning.Examples.Models.Person]]". The same schemaId is already used for type "$Asp.Versioning.OData.ODataValue1[System.Collections.Generic.IEnumerable1[ApiVersioning.Examples.Models.Person]]"
at Swashbuckle.AspNetCore.SwaggerGen.SchemaRepository.RegisterType(Type type, String schemaId)
at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.GenerateReferencedSchema(DataContract dataContract, SchemaRepository schemaRepository, Func1 definitionFactory) at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.GenerateConcreteSchema(DataContract dataContract, SchemaRepository schemaRepository) at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.GenerateSchema(Type modelType, SchemaRepository schemaRepository, MemberInfo memberInfo, ParameterInfo parameterInfo, ApiParameterRouteInfo routeInfo) at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateSchema(Type type, SchemaRepository schemaRepository, PropertyInfo propertyInfo, ParameterInfo parameterInfo, ApiParameterRouteInfo routeInfo) --- End of inner exception stack trace --- at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateSchema(Type type, SchemaRepository schemaRepository, PropertyInfo propertyInfo, ParameterInfo parameterInfo, ApiParameterRouteInfo routeInfo) at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.CreateResponseMediaType(ModelMetadata modelMetadata, SchemaRepository schemaRespository) at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable1 source, Func2 keySelector, Func2 elementSelector, IEqualityComparer1 comparer) at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateResponse(ApiDescription apiDescription, SchemaRepository schemaRepository, String statusCode, ApiResponseType apiResponseType) at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateResponses(ApiDescription apiDescription, SchemaRepository schemaRepository) at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository) --- End of inner exception stack trace --- at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository) at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperations(IEnumerable1 apiDescriptions, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GeneratePaths(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwaggerDocumentWithoutFilters(String documentName, String host, String basePath)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwaggerAsync(String documentName, String host, String basePath)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.OData.Routing.ODataRouteDebugMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

@commonsensesoftware
Copy link
Collaborator

The main issue here is that you haven't updated all of the templates. There are a few actions, specifically NewHires, which requires an explicit route template. I don't know the exact reason, but it has to do with matching the OData conventions. If you don't do it that way, it will not be considered an OData action. This can be confirmed via the ~/$odata debug endpoint. To fix things, you just need to change the prefix from api/ to api/v{version:apiVersion} on those actions and it will work as expected.

I did notice something else that was unexpected and I think is a bug. The version neutral endpoint also needs to have it's route template updated or it will not be an OData action either. However, updating the template does not substitute the API version in the route template as expected. I'm not why that is happening, but it's supposed to work. If you don't have any version-neutral endpoints, then that is a non-issue for you. If you do, then you'll have to provide your own fix in the Swashbuckle extensions until such time as there is a fix for it.

@rdelmont
Copy link
Author

Thank you for your answer.
Changing the prefix for the remaining actions works perfectly.
Though I'm having a hard time making it work on my project. We are using attribute routing, required by the [ApiController] attribute and I cannot find the right combination to have Swagger generation work fine.

I always end up getting:
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Failed to generate Operation for action - Services.V1.Controllers.PiecesController.CreateAsync (Services). See inner exception
---> Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Failed to generate schema for type Services.V1.Dtos.PieceCreateDto. See inner exception
---> System.InvalidOperationException: Can't use schemaId "$Services.V1.Dtos.ViewerDataDto" for type "$Services.V1.Dtos.ViewerDataDto". The same schemaId is already used for type "$Services.V1.Dtos.ViewerDataDto"

If you have any clue, it will be much appreciated.

@commonsensesoftware
Copy link
Collaborator

I believe you can use attribute routing, but it still has to match the OData conventions or they don't line up. You can verify that by visiting the ~/$odata debug endpoint. I, personally, found it to be too much work and I just follow whatever the OData conventions are. When I possible, I don't bother with defining the route templates. In some cases, I've found the conventions don't seem to match up if you don't, but perhaps I'm doing something wrong.

The only way you can get this error is that there are two different definitions for ViewerDataDto appearing in the same EDM and they have different shapes. They need to either be exactly the same or have different names. You can define a different name in the EDM for the entity type, complex type, or however it's being used. To the client, it will appear as different types, but on the server, they will be the same.

You also shouldn't use the Async suffix in action names. This was a breaking change first introduced in ASP.NET Core 3.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants