Skip to content

Using v1 pattern in url for minimal API .NET 6 versioning #830

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
Sonic3R opened this issue May 18, 2022 · 12 comments
Closed

Using v1 pattern in url for minimal API .NET 6 versioning #830

Sonic3R opened this issue May 18, 2022 · 12 comments

Comments

@Sonic3R
Copy link

Sonic3R commented May 18, 2022

Hi

I want to use /v1/orders or /v2/orders pattern using api versioning

Well, I configured like:

var builder = WebApplication.CreateBuilder(args);
ApiVersion apiV1 = new ApiVersion(1);
ApiVersion apiV2 = new ApiVersion(2);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning(opts =>
{
    opts.ReportApiVersions = true;
    opts.ApiVersionReader = new UrlSegmentApiVersionReader();
}).AddApiExplorer(opts =>
{
    opts.GroupNameFormat = "'v'VVV";
    opts.SubstituteApiVersionInUrl = true;
});

builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen();

// other DI calls

var app = builder.Build();
var orderApiSet = app.NewApiVersionSet("Orders").Build();

app.MapGet("/orders", async ([FromServices] IOrdersRepository orderRepo) =>
{
    return await orderRepo.ListAsync(...);
}).WithApiVersionSet(orderApiSet)
  .HasApiVersion(apiV1);

app.MapGet("/orders", async ([FromServices] IOrdersRepositoryV2 orderRepo) =>
{
    return await orderRepo.List1Async(...);
}).WithApiVersionSet(orderApiSet)
  .HasApiVersion(apiV2);


app.UseSwagger();
app.UseSwaggerUI(opts =>
{
        var descriptions = app.DescribeApiVersions();
        foreach (var desc in descriptions)
        {
            var url = $"/swagger/{desc.GroupName}/swagger.json";
            var name = desc.GroupName.ToUpperInvariant();
            opts.SwaggerEndpoint(url, $"Orders API {name}");
        }
});

await app.RunAsync();

I expected if type in url like /v1/orders that would go to first MapGet but I got 404 page. As well as for /v2/orders

If I type /orders without any vX then I got 400 that api version is not specified.

How to solve this ?

A solution would be app.MapGet("v{version:apiVersion}/orders") but in SwaggerUI displays like:

image

But is not quite good because I already selected api version from top right corner dropdown, don't see the purpose of {version} in call

@commonsensesoftware
Copy link
Collaborator

Thanks for reporting this. There are lot of possible setup variations and I hadn't tried this one. There is a bug in ASP.NET Core's implementation of the API Explorer for Minimal APIs that is causing this. I've reported it so we'll see what they say. API Versioning relies on the ApiParameterDescription being discovered and populated so that it can match it up for substitution. The parameter is not discovered so there is nothing to match.

In the meantime, there are a few possible workarounds. The best option at the moment would probably be to use a custom IOperationFilter in Swashbuckle and perform the substitution yourself. That will be predicated on using the same route parameter name in your templates (e.g. {version}, but you define it).

  • The ApiDescription.GetApiVersion() extension method with get you the associated ApiVersion
  • ApiVersion.ToString can then be called using ApiExplorerOptions.SubstitutionFormat to get the substitution value
  • Replace {version} (or appropriate token) in ApiDescription.RelativePath

This effectively the same steps API Versioning uses. Without a match parameter though, there's no way to accurately know how to generate the {<name>} token for substitution.

@commonsensesoftware
Copy link
Collaborator

The ASP.NET Core team confirmed the bug, but they've yet to provide any feedback as to when the fix might be published. I'm getting the impression that it may not happen until .NET 7.0. It's a pretty glaring oversight IMHO and should be included in a service release. The bug affects more than just API Versioning.

Regardless, I've come up with a way to work around the problem and hide the internal yuckiness of it. It's a sticking point for me that the fix should not result in a change (e.g. breaking change) to API Versioning's public API surface. It's not the ideal solution, but I've got something working that will make this scenario work without exposing the bits that will likely go away in a future patch or version of ASP.NET Core.

Expect to see this fixed in the next release.

@commonsensesoftware
Copy link
Collaborator

@knightian is that a question? Looks like you are probably missing a using directive. The recommended statements are:

using Asp.Versioning;
using Asp.Versioning.Conventions;

@knightian
Copy link

knightian commented Jul 21, 2022

@knightian is that a question? Looks like you are probably missing a using directive. The recommended statements are:

using Asp.Versioning;
using Asp.Versioning.Conventions;

Entire Asp.Versioning.Mvc.ApiExplorer was not even added to my project by nuget. Seems it needed to be manually added.

Didn't think I would be using Mvc stuff for minimal?

@commonsensesoftware
Copy link
Collaborator

Yeah, it's unfortunate, but the much of the implementation for the API Explorer (Microsoft.AspNetCore.Mvc.ApiExplorer) depends on Microsoft.AspNetCore.Mvc.Core. I'm not entirely sure why that is, but it's a decision that was made years ago and can't easily change. The core abstractions - Microsoft.AspNetCore.Mvc.Abstractions - is part of the MVC Core surface area, but doesn't have a dependency on the Microsoft.AspNetCore.Mvc.Core.

I considered attempting to split the two apart, but it wasn't buying much value. I did move the abstractions into Asp.Versioning.Http, which is the bare requirement for Minimal APIs. I also didn't want to repeat the mistakes of the ASP.NET team will reimplemented some API Explorer implementation and missed supported use cases. I purposely kept Mvc in the name so there is a way to break things a part in the future if it ever makes sense. For this particular use case, there really isn't any MVC being used. Since the ASP.NET Core model is a now a FrameworkReference as opposed to individual packages, it shouldn't be of any concern; however, I did try to keep things as isolated as possible.

There are arguably a few things under the MVC name that aren't really MVC. Term Web API is still used even though it's branded under MVC. If Swagger/OpenAPI would have been more mature - say 5+ years ago, I think you might have seen things unfold and be named differently. I hope that helps clear things up.

@commonsensesoftware
Copy link
Collaborator

6.0 has been officially released and contains the workaround. In terms of the original question, that should address it. I can't control when the ASP.NET Core will eventually provide a fix, but once they do, I'll integrate it. That change should be transparent to API Versioning consumers.

There are some additional improvements for .NET 7 that should allow declaring an ApiVersion parameter, which might help. We'll have to see.

Thanks for reporting the issue and the discussion. If somehow this did not solve your problem, I'm happy to reopen the issue and discuss further.

@jamesfoster
Copy link

jamesfoster commented Nov 19, 2022

Hi, I'm getting the same error.

My swagger set up is taken from the example projects.

Asp.Versioning.Mvc: 6.2.1
Asp.Versioning.Mvc.ApiExplorer: 6.2.1
Swashbuckle.AspNetCore: 6.4.0
Target Framework: net6.0

[Route("/v{apiVersion}")]
[ApiVersion("2.0")]
public class ListCountryCodesController_v2
{
    [HttpGet("country-codes", Name = Routes.ListCountryCodes_v2)]
    [MapToApiVersion("2.0")]
    public ActionResult<List<CountryCodeViewModel>> GetAll()
    {
        // implementation omitted
    }
}
        services
            .AddControllers();

        services
            .AddApiVersioning(options =>
            {
                options.ApiVersionReader = new UrlSegmentApiVersionReader();
                options.DefaultApiVersion = new ApiVersion(1, 0);
                options.ReportApiVersions = true;
            })
            .AddMvc()
            .AddApiExplorer(options =>
            {
                options.GroupNameFormat = "'v'VVVV";
                options.SubstituteApiVersionInUrl = true;
            });

        services.AddTransient<IApiControllerSpecification, AllApiControllerSpec>(); // treat all controllers as "API controllers"

image

@commonsensesoftware
Copy link
Collaborator

@jamesfoster This is happening because you are missing the route constraint. You have:

[Route("/v{apiVersion}")]

but it needs to be:

[Route("/v{version:apiVersion}")]

apiVersion is the default name of the ApiVersionRouteConstraint. This can be changed via ApiVersioningOptions.RouteContraintName, but I don't think I've seen anyone actual do it. The name is used by infrastructure so the option to configure it ensures they keep in lock-step.

You can name the route parameter whatever you want. In the example it's version, but it can be anything you like (e.g. apiVersion, ver, etc).

Also note that the name ListCountryCodesController_v2 is against the expected naming convention. It's expected to have the form ListCountryCodes2Controller. The default naming convention that ASP.NET Core uses assumes the name will be [Name]Controller. API Versioning extends this convention to be [Name][#]Controller. If you go off convention, it may break collation because it happens by name. Applying [ControllerName("<name>")] or swapping out the IControllerNameConvention service are alternate ways to give yourself more flexibility in type names.

@jamesfoster
Copy link

Thanks, that worked. Much appreciated. I realised the name of the controller was causing it not to be picked up. I'm probably going to organise the files in folders/namespaces by version. Thanks so much for your help.

@djrietberg
Copy link

djrietberg commented Nov 28, 2022

@commonsensesoftware Hi there sorry to disturb, I seem to be running into the same issue as above.
I am running a dotnet7 minimal-api project with swagger and all my apis show "v{version}" in the url.
Problem is that with minimal api I use "AddEndpointsApiExplorer" instead of "AddApiExplorer" which means i can;t set the option for the quick fix you've implemented, is there any guidance you can give on how to work around this using the minimal API. (or maybe i am missing something in the configuration)

@commonsensesoftware
Copy link
Collaborator

@djrietberg I think you may be confused as to which AddApiExplorer you are looking for. 😉

builder.Services.AddEndpointsApiExplorer();                                                      // ← ASP.NET Core

builder.Services
       .AddApiVersioning(options => options.ApiVersionReader = new UrlSegmentApiVersionReader()) // ← API Versioning
       .AddApiExplorer(options =>                                                                // ← API Versioning
        {
            options.GroupNameFormat = "'v'VVV";
            options.SubstituteApiVersionInUrl = true;
        });

IApiVersioningBuilder.AddApiExplorer does something different from IMvcCoreBuilder.AddApiExplorer.

You always need builder.Services.AddApiVersioning().AddApiExplorer() regardless of whether you are using Minimal APIs or MVC Core with controllers.

@djrietberg
Copy link

You are absolutely right, thanks... small distinctions make a big difference.. as always.. Many thanks for the quick reply..

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

5 participants