Skip to content

Asp Net Core + OData + Swagger + Url Segment Versioning #365

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
SuricateCan opened this issue Oct 9, 2018 · 4 comments · Fixed by #384
Closed

Asp Net Core + OData + Swagger + Url Segment Versioning #365

SuricateCan opened this issue Oct 9, 2018 · 4 comments · Fixed by #384

Comments

@SuricateCan
Copy link

SuricateCan commented Oct 9, 2018

Hi all,

I'm having trouble setting up versioning by URL segment. After figuring out how to add the version to the route template, I'm faced with these situations:

  1. The routing works as expected. Each call is directed to the correct version of the api;
  2. Swagger does not recognize the template, and instead of registering routes like this api/v1/values it is showing like this api/v{version:apiVersion}/values which, as expected, causes a lot of troubles;
  3. And finally, when trying to show the document for another version, I get and exception saying my routes are not unique.

Here is how I've setup the application:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddApiVersioning(options =>
    {
        options.ReportApiVersions = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    });
    services.AddOData().EnableApiVersioning();
    services.AddODataApiExplorer(
        options =>
        {
            options.GroupNameFormat = "'v'VVV";
            options.SubstituteApiVersionInUrl = true;
        });

    services.AddSwaggerGen(a =>
    {
        var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();

        foreach (var description in provider.ApiVersionDescriptions)
        {
            var info = new Info
            {
                Title = "My API Title",
                Version = description.ApiVersion.ToString(),
                Description = "My API Description"
            };

            if (description.IsDeprecated)
                info.Description += " NOTE: This API has been deprecated";

            a.SwaggerDoc(description.GroupName, info);
        }

        a.ParameterFilter<SwaggerDefaultValues>();

        var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, $"{PlatformServices.Default.Application.ApplicationName}.xml");
        a.IncludeXmlComments(filePath);

        a.DescribeAllEnumsAsStrings();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, VersionedODataModelBuilder versionedModelBuilder, IApiVersionDescriptionProvider versionDescriptionProvider)
{
    app.UseMvc(routeBuilder =>
    {
        routeBuilder.Select().Expand().Filter().OrderBy().Count();
        routeBuilder.MapVersionedODataRoutes("odata", "api/v{v:apiVersion}", versionedModelBuilder.GetEdmModels());
    });
    app.UseSwagger();
    app.UseSwaggerUI(a =>
    {
        // build a swagger endpoint for each discovered API version
        foreach (var description in versionDescriptionProvider.ApiVersionDescriptions)
        {
            a.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName);
        }
    });
}

SwaggerDefaultValues.cs

public class SwaggerDefaultValues : IParameterFilter
{
    public void Apply(IParameter parameter, ParameterFilterContext context)
    {
        if (!(parameter is NonBodyParameter nonBodyParameter))
            return;

        if (nonBodyParameter.Description == null)
        {
            nonBodyParameter.Description = context.ApiParameterDescription.ModelMetadata?.Description;
        }

        if (context.ApiParameterDescription.RouteInfo == null)
            return;

        if (nonBodyParameter.Default == null)
        {
            nonBodyParameter.Default = context.ApiParameterDescription.RouteInfo.DefaultValue;
        }

        parameter.Required |= !context.ApiParameterDescription.RouteInfo.IsOptional;
    }
}

So, am I doing something wrong? Is this scenario supported?

Thanks in advance.

@commonsensesoftware
Copy link
Collaborator

This looks correct. I'll give your setup a whirl to see if I can repro the bad or correct behavior. It's quite possible that there is a bug.

@SuricateCan
Copy link
Author

Thanks @commonsensesoftware
I've managed to get it working, but with a major work around. I'm not even sure it is safe to do what I did.
I've added an OperationFilter and a DocumentFilter to replace the pattern in the paths with each version:

public const string VersionPattern = "v{v:apiVersion}";
//operation filter
public void Apply(Operation operation, OperationFilterContext context)
{
    context.ApiDescription.RelativePath = context.ApiDescription.RelativePath.Replace(VersionPattern, context.ApiDescription.GroupName);
}
//document filter
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
    var pathItems = new Dictionary<string, PathItem>();
    var toRemove = new List<string>();

    foreach (var swaggerDocPath in swaggerDoc.Paths)
    {
        foreach (var apiDescription in _apiVersionDescriptionProvider.ApiVersionDescriptions)
        {
            if (swaggerDocPath.Key.Contains(VersionPattern))
            {
                pathItems.Add(
                    swaggerDocPath.Key.Replace(VersionPattern, apiDescription.GroupName),
                    swaggerDocPath.Value);

                toRemove.Add(swaggerDocPath.Key);
            }
        }
    }

    foreach (var key in toRemove.Distinct())
    {
        swaggerDoc.Paths.Remove(key);
    }

    foreach (var pathItem in pathItems.Reverse())
    {
        swaggerDoc.Paths.Add(pathItem);
    }
}

I did not test with multiple versions yet though and I'd love to hear your feedback.

@commonsensesoftware
Copy link
Collaborator

I've been able to confirm that this is actually a bug. Special template parsing and replacement is required for the OData route prefix. I thought this would be a really, really quick fix, but it's turning out to be a little more complex than I thought. I should have the fix out for beta 2 in the next couple of days. In the meantime, your workaround should be suitable. This is more or less what was required before the built-in substitution option was available.

@SuricateCan
Copy link
Author

Great. I'll give beta 2 a try when it's out and post my results. Thanks

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

Successfully merging a pull request may close this issue.

2 participants