Swashbuckle: 5.6.0
AspNet.WebApi.Versioning 2.2.0
Consume any endpoint (postman or swagger)
Hits the actual endpoint
Receive the error:
ApiVersionUnspecified
Using Owin with WebAPI. Owin's Setup is blank.
Global.asax.cs:
protected void Application_Start(object sender, EventArgs e)
{
GlobalConfiguration.Configure((GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(WebApiConfig)) as WebApiConfig).Register);
}
WebApiConfig.cs:
public void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver
{
ConstraintMap =
{
["apiVersion"] = typeof(ApiVersionRouteConstraint)
}
};
config.MapHttpAttributeRoutes(constraintResolver);
config.AddApiVersioning(o => o.ReportApiVersions = true);
this.restApiConfig.Register(config);
}
Controller route formatting:
[ApiVersion("3.0")]
[RoutePrefix("v3")]
public class SomeController {[Route("/someRoute")]
public HttpResponseMessage GetStuff(){ }
We have attempted to use the [RoutePrefix(v{version : apiVersion)] attribute, which works with actually making the endpoints function under versioning, but forces Swagger to include a version parameter, which we do not want. We want our paths to be "/v2/controller/action" or "/v3/controller/action".
Happy to use the [RoutePrefix(v{version : apiVersion)] method, as long as we can display the paths properly in Swagger without having to input a version parameter. Otherwise, if we could use the [RoutePrefix("v3")] method without getting ApiVersionUnspecified errors, that would be even better.
Is there a way to use our preferred [RoutePrefix("v3")] method?
API versioning does not know about nor parse _magic_ strings. It uses a route constraint so that you can define where and how the value appears in your URLs. In a similar way, the prefix v is not actually part of an API version. In order for the infrastructure to pick up and validate the API version when versioning by URL segment, you must use the route constraint; otherwise, API versioning can't extract the value from the request.
From your description, the issue is really with Swagger/Swashbuckle. The API Explorer provided by API versioning will generate the appropriate parameter and default value for the API version route parameter. However, if that's now how _you_ want to see that in Swagger, then you'll need some customization to transform the operation which Swashbuckle is generating the document. Any time that Swashbuckle passes you an ApiDescription, this will actually be an instance of the super type VersionedApiDescription. Casting to VersionedApiDescription will give you access to the corresponding ApiVersion as well as the RelativePath (ex: v{version}/someRoute). Some minor string manipulation will allow you to update the RelativePath with the corresponding API version value populated. You can format the API version yourself or rely on the supported, built-in formatting capabilities. You'll also want to remove the parameter definition for the API version.
Another possibility is subclass the VersionedApiExplorer, override the ApiDescriptions, and perform the same transformation. You could also use the _Decorator_ pattern to create an IApiExplorer implementation that wraps over VersionedApiExplorer and performs the transformation. There isn't anything special about wiring up an API Explorer. The extension methods are just shortcuts. You custom implementation might look like:
c#
var apiExplorer = new UrlVersionedApiExplorer();
configuration.Services.Replace(typeof(IApiExplorer), apiExplorer);
// remaining Swagger configuration
Yet one more way to solve the problem is to disable the input for the API version parameter in the Swagger in the UI. I don't believe Swashbuckle has the capability to mark a parameter as read-only, but this should be technically feasible. If the value is read-only, I'm not sure that it's worth the effort to customize other pieces because the constructed URL will be correct. I suppose it's possible to make the input for the parameter hidden as well.
As you can see, there are a few ways to achieve your desire result. Choose whichever works best for you.
I hope that helps.
This does help. There was some real confusion as to WHY it was failing. The route we're testing out now is similar to what you're describing with UrlVersionApiExplorer.
Will report back a final solution. Thanks!
A follow-up: it looks like VersionedApiDescription is derived from ApiDescription rather than the other way around. I don't believe we can cast up, right? Or am I missing something?
I'm also running into the following error. I suspect it has to do with when I'm registering the VersionedApiExplorer. I've run into this issue every time I've tried to utilize any version of the ApiExplorer, actually.
System.ArgumentException
HResult=0x80070057
Message=A route named 'FiltersGetAction' is already in the route collection. Route names must be unique.
Parameter name: name
Source=System.Web.Http.WebHost
StackTrace:
at System.Web.Http.WebHost.Routing.HostedHttpRouteCollection.Add(String name, IHttpRoute route)
at System.Web.Http.Routing.AttributeRoutingMapper.AddGenerationHooksForSubRoutes(HttpRouteCollection routeTable, IEnumerable1 entries)
at System.Web.Http.Routing.AttributeRoutingMapper.<>c__DisplayClass2.
at System.Web.Http.HttpConfiguration.ApplyControllerSettings(HttpControllerSettings settings, HttpConfiguration configuration)
at System.Web.Http.Controllers.HttpControllerDescriptor.InvokeAttributesOnControllerType(HttpControllerDescriptor controllerDescriptor, Type type)
at System.Web.Http.Controllers.HttpControllerDescriptor.InvokeAttributesOnControllerType(HttpControllerDescriptor controllerDescriptor, Type type)
at System.Web.Http.Controllers.HttpControllerDescriptor..ctor(HttpConfiguration configuration, String controllerName, Type controllerType)
at Microsoft.Web.Http.Description.VersionedApiExplorer.FlattenApiVersions()
at Microsoft.Web.Http.Description.VersionedApiExplorer.InitializeApiDescriptions()
at System.Lazy1.CreateValue()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Lazy1.get_Value()
at Microsoft.Web.Http.Description.VersionedApiExplorer.get_ApiDescriptions()
at RIS_REST_Services.UrlVersionedApiExplorer.get_ApiDescriptions() in C:\dev\RightslineGit\Services\RIS_REST_Services\App_Start\SwaggerConfig.cs:line 325
at RIS_REST_Services.SwaggerConfig.<>c__DisplayClass0_0.
at Swashbuckle.Application.SwaggerDocsConfig.MultipleApiVersions(Func3 versionSupportResolver, Action1 configure)
at RIS_REST_Services.SwaggerConfig.<>c__DisplayClass0_0.
at Swashbuckle.Application.HttpConfigurationExtensions.EnableSwagger(HttpConfiguration httpConfig, String routeTemplate, Action1 configure)
at Swashbuckle.Application.HttpConfigurationExtensions.EnableSwagger(HttpConfiguration httpConfig, Action1 configure)
at RIS_REST_Services.SwaggerConfig.Register() in C:\dev\RightslineGit\Services\RIS_REST_Services\App_Start\SwaggerConfig.cs:line 31
at RIS_REST_Services.App_Start.WebApiConfig.Register(HttpConfiguration config) in C:\dev\RightslineGit\Services\RIS_REST_Services\App_Start\WebApiConfig.cs:line 34
at System.Web.Http.GlobalConfiguration.Configure(Action1 configurationCallback)
at RIS_REST_Services.Global.Application_Start(Object sender, EventArgs e) in C:\dev\RightslineGit\Services\RIS_REST_Services\Global.asax.cs:line 25
I'm not sure what you have, but it should be something like this:
```c#
public class UrlVersionedApiExplorer : VersionedApiExplorer
{
public UrlVersionedApiExplorer( HttpConfiguration configuration )
: base(configuration, new ApiExplorerOptions(configuration) {GroupNameFormat = "'v'VVV"})
{
}
protected override ApiDescriptionGroupCollection InitializeApiDescriptions()
{
var groups = base.InitializeApiDescriptions();
foreach ( var group in groups )
{
foreach ( var description in group.ApiDescriptions )
{
var url = description = description.RelativePath;
url = url .Replace( "{version}", description.ApiVersion.ToString("VVV") );
description.RelativePath = url;
}
}
return groups;
}
}
Then in your configuration, it should look something like:
```c#
var apiExplorer = new UrlVersionedApiExplorer( configuration );
configuration.Services.Replace( typeof( IApiExplorer ), apiExplorer );
// remaining swagger config, which is also shown in the examples
@commonsensesoftware
My set up for the UrlVersionedApiExplorer is the same as you have it. I'm adding it to the service in my WebApiConfig, which is called from the App_Start of Global.ascx. The issue, I think, is something to do with either the timing of when the versioned explorer is being created or perhaps it's conflicting with Owin or some other package, but I can't fathom what. I can't find any reference to FiltersGetAction, online (google gets me nothing useful), but I only see the exception when attempting to use any form of the ApiExplorer.
I realize it isn't always easy, but if you have any kind of repro or source you can share, that would be really useful at trying to help troubleshoot this.
Based on the stack trace, I would surmise any of the following could be happening:
[Route(Name='FiltersGetAction')] is declared on more than one controller or action.Best as I can tell, the source of the route name appears to be coming from a controller. Hopefully, some piece of information here will help you track it down.
FilterGetActions doesn't even exist on our code base. That's the thing that's making me crazy. It's got to be coming from competing nuget packages, I suppose. There's no reference to it in our code base.
Let me see if I can put together a paired down version of our set up to post here.
Do you have controllers coming from an external source (e.g. package or something)? Those should not be duplicated.
Aside from the _nuclear option_ like debugging with Reflector, you can try whipping together and replacing the DefaultDirectRouteProvider.
Then you can register it like:
c#
configuration.MapHttpAttributeRoutes( new DebugDirectRouteProvider() );
This won't solve the problem, but it will allow you to debug and trace all the routes being generated from attributes (formally known as _direct routes_). That should help identify the offending controller.
I'll give that a shot right now.
It looks like the issue was an unused reference to the nuget WebHooks packages. I removed those and it fixed the FilterGetAction issue. I'm working through getting the version explorer to implement properly now.
Glad you were able to track down the culprit
This issue is resolved. Now onto a new issue. Ugh!
FYI, there is a new version of the API Explorer package available. I've added an option called SubstituteApiVersionInUrl, which enables the behavior you want without further modification.
c#
AddVersionedApiExplorer( options => options.SubstituteApiVersionInUrl = true );
The corresponding wiki topic and examples now reflect this too.
Trying to see this working in a default .net core project ValuesController with a route attribute on the controller
```c#
[Route("api/v{version:apiVersion}/[controller]")]
And the following ApiExplorerOptions
```c#
services.AddMvcCore()
.AddApiExplorer()
.AddVersionedApiExplorer(c =>
{
c.GroupNameFormat = "'v'VVV";
c.SubstituteApiVersionInUrl = true;
})
I expected to see /api/v1.0/Values/{id} in Swagger UI, but instead I still see /api/v{version}/Values/{id}
Am I missing the point?
This _looks_ correct. I'm afraid I'll need some more context. Have you reviewed the ASP.NET Core Swagger Sample? It uses this style for demonstration purposes and definitely works.
One thing I notice is that you have a call to AddApiExplorer, which is not needed. I can't say for sure, but that might be affecting the results. I would consider removing it.
I hope that helps.
Without services.AddMvc() I needed to add AddApiExplorer otherwise there is no api discovery (Swagger is empty). I found this from https://github.com/Microsoft/aspnet-api-versioning/issues/163#issuecomment-314847877
Narrowing down the code, the following ApiVersionReader code is the culprit that breaks it...
c#
c.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("api-version"), // svc?api-version=2.0
new MediaTypeApiVersionReader(), // Content-Type: application/json;v=2.0
new HeaderApiVersionReader("api-version", "x-ms-version") // new List<string>() { "api-version" }
);
Providing the version readers somehow breaks the assignment of the version substitution in the url.
Adding a new UrlSegmentApiVersionReader() provides Swagger with the necessary method for getting the version.
This works as expected...
c#
c.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("api-version"), // svc?api-version=2.0
new UrlSegmentApiVersionReader(),
new MediaTypeApiVersionReader(), // Content-Type: application/json;v=2.0
new HeaderApiVersionReader("api-version", "x-ms-version") // new List<string>() { "api-version" }
);
That makes complete sense to me. Each reader also implements the source from which an API version is read. If you remove or change the default readers , you change the behavior. Without the UrlSegmentApiVersionReader, you've basically removed versioning by URL.
The API Explorers rely on the parameter source and, hence, the readers to provide the parameters. The substitution is based on a parameter being added that comes from the URL path. If no such parameter is found, there's nothing for the API Explorer to substitute.
Most helpful comment
FYI, there is a new version of the API Explorer package available. I've added an option called
SubstituteApiVersionInUrl, which enables the behavior you want without further modification.c# AddVersionedApiExplorer( options => options.SubstituteApiVersionInUrl = true );The corresponding wiki topic and examples now reflect this too.