Aspnet-api-versioning: AssumeDefaultVersionWhenUnspecified is not working as expected

Created on 27 Sep 2018  路  7Comments  路  Source: microsoft/aspnet-api-versioning

I have been using asp net core versioning component for my WebAPI. Need your help in understanding how AssumeDefaultVersionWhenUnspecified is working. (tried searching for documentation, but couldn't find one)

My startup looks like below

services.AddApiVersioning(o => {
            o.ReportApiVersions = true;
            o.AssumeDefaultVersionWhenUnspecified = true;
            o.DefaultApiVersion = new ApiVersion(2, 0);
            o.ApiVersionReader = new UrlSegmentApiVersionReader();
        });

When the route attribute is something like below

[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/values")]
[ApiController]
public class ValuesV2Controller : ControllerBase
{
...
}

The above route works only when the api version is specified. ie: http://localhost:55401/api/v2/values If I call like http://localhost:55401/api/values, getting 404 error

My question is this. How AssumeDefaultVersionWhenUnspecified works. Wouldn't it ignore the version in Route? Looks like Route attribute takes precedence over AssumeDefaultVersionWhenUnspecified. If I choose QueryString or Header versioning and when the Route looks like

[ApiVersion("2.0")]
[Route("api/values")]

the default routing reaches the API (when the version is not specified in the query string or header)

Am I missing anything or is my understanding wrong? How shall I achieve default routing to the latest version API using url versioning?

question

Most helpful comment

In short, you can't. Since this style of versioning uses a URL segment, there is no way to omit part of the path. By definition of the _Uniform Interface_ REST constraint, the path is the identifier so it doesn't really make sense that you can default or omit part of it.

There is a way to make it work as explained here in the wiki. You will have to define multiple routes, effectively an _alternate identiifer_, where both routes map to the same action. One will have the API version in the path and the other will not. When the path without the API version is requested, the lack of an API version will be allowed and assumed to be the configured, default API version (as you have shown).

It also appears that you want to _float_ this behavior to your latest version. Keep in mind that if you do that, you run the risk of breaking older clients because they won't know they've been redirected to a nwe API version. Any API change, even additive, is not a guarantee that a client will not break. You will also have to move the route declaration without the API version segement to the latest controller implementation. You probably want to set the option:

options.ApiVersionSelector = new CurrentImplementationApiVersionSelector().

This will select the maximum API version (e.g. the _current_ one) so that you don't have to keep changing options.DefaultApiVersion which is used in several scenarios, not just for assuming the default version.

This limitation only applies to the URL segment method because it uses the path. All of the other supported versioning methods can support your requirements without having to do any other special setup.

I hope that helps.

All 7 comments

In short, you can't. Since this style of versioning uses a URL segment, there is no way to omit part of the path. By definition of the _Uniform Interface_ REST constraint, the path is the identifier so it doesn't really make sense that you can default or omit part of it.

There is a way to make it work as explained here in the wiki. You will have to define multiple routes, effectively an _alternate identiifer_, where both routes map to the same action. One will have the API version in the path and the other will not. When the path without the API version is requested, the lack of an API version will be allowed and assumed to be the configured, default API version (as you have shown).

It also appears that you want to _float_ this behavior to your latest version. Keep in mind that if you do that, you run the risk of breaking older clients because they won't know they've been redirected to a nwe API version. Any API change, even additive, is not a guarantee that a client will not break. You will also have to move the route declaration without the API version segement to the latest controller implementation. You probably want to set the option:

options.ApiVersionSelector = new CurrentImplementationApiVersionSelector().

This will select the maximum API version (e.g. the _current_ one) so that you don't have to keep changing options.DefaultApiVersion which is used in several scenarios, not just for assuming the default version.

This limitation only applies to the URL segment method because it uses the path. All of the other supported versioning methods can support your requirements without having to do any other special setup.

I hope that helps.

Fantastic @commonsensesoftware. Thank you so much for the reply. This helps me a lot

@commonsensesoftware, could you give advice to one more thing? As you explained, I prepare custom routes for every controller in my WebAPI and every controller contains:

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

This solution works, but there is one thing that cannot be accepted in my case. My solution is using Swagger and now every one controller contains actions like:
image

Is there any solution to:
a) show action without version when opened is default version on swagger document.
b) show action with version url part when opened is version which is not default ?

You sure can. It looks like you're using Swashbuckle. My understanding is that you'll need to do this with your own, custom IDocumentFilter.

It will look something like:

```c#
public class DefaultApiVersionFilter : IDocumentFilter
{
public DefaultApiVersionFilter( ApiVersion defaultApiVersion, string apiVersionFormat )
{
DefaultApiVersion = defaultApiVersion;
ApiVersionFormat = apiVersionFormat;
}

// this should the same as the DefaultApiVersion option
protected ApiVersion DefaultApiVersion { get; }

// this should be the same as the SubstitutionFormat api explorer option
protected string ApiVersionFormat { get; }

public void Apply(
SwaggerDocument swaggerDoc,
SchemaRegistry schemaRegistry,
IApiExplorer apiExplorer)
{
// take the guess work out of formatting the version the same way as the api explorer
var versionSegment = DefaultApiVersion.ToString( ApiVersionFormat );

foreach ( var apiDescription in apiExplorer.ApiDescriptions.OfType<VersionedApiDescription>() )
{
  if ( apiDescription.ApiVersion == DefaultApiVersion &&
       apiDescription.RelativePath.Contains( versionSegment ) )
  {
    // remove the path that contains the versioned url segment
    var path = "/" + apiDescription.RelativePath.TrimEnd( '/' );
    swaggerDoc.paths.Remove( path );
  }
}

}
}
```

I hope that helps.

Thank, I help but do I'm struggling one more thing. I am using Swashbuckle.AspnetCore. The IDocument filter do not implement IApiExplorer, so I modify the code and my Apply looks like:

        public void Apply(
            SwaggerDocument swaggerDoc, 
            DocumentFilterContext context)
        {
            var versionSegment = DefaultApiVersion.ToString(ApiVersionFormat);

            foreach (var apiDescription in context.ApiDescriptions.OfType<ApiDescription>())
            {
                if (apiDescription.GetApiVersion() == DefaultApiVersion &&
                    apiDescription.GroupName.Contains(versionSegment))
                {
                    // remove the path that contains the versioned url segment
                    var path = "/" + apiDescription.GroupName.TrimEnd('/');
                    swaggerDoc.Paths.Remove(path);
                }
            }
        }

But now, I have problem with start the app. I get
Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.ApiVersion' while attempting to activate 'Nofo.WebApi.Web.Core.Extensions.Swagger.DefaultApiVersionFilter'.

I used Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer 2.2.0 and Swashbuckle.AspnetCore 3.0 and my usings are:

using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;

My AddSwaggerGen looks like:

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

                foreach (var description in provider.ApiVersionDescriptions)
                {
                    c.SwaggerDoc(description.GroupName, CreateApiVersionInfo(description));
                }

                c.DocumentFilter<DefaultApiVersionFilter>();

This is due to how your filter is being registered. It is configured with dependency injection and the runtime is unable to resolve your constructor's parameters. You should be able to update it like this:

```c#
private readonly IOptions options;

public DefaultApiVersionFilter( IOptions options ) => this.options = options;

protected ApiVersion DefaultApiVersion => options.Value.DefaultApiVersion ;

protected string ApiVersionFormat => options.Value.SubstitutionFormat;
```

@commonsensesoftware Many thanks for your help! Now everything works great!

I've changed a little logic to be working with .NET Core. Now if the version is default swagger document do not show path with the version, else only paths with version are shown. Please find what the final solution, maybe someone will be interested.

using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace MyProject.Core.Extensions.Swagger
{
    public class DefaultApiVersionFilter : IDocumentFilter
    {
        private readonly IOptions<ApiExplorerOptions> _options;

        public DefaultApiVersionFilter(IOptions<ApiExplorerOptions> options)
        {
            _options = options;
        }

        protected ApiVersion DefaultApiVersion => _options.Value.DefaultApiVersion;
        protected string ApiVersionFormat => _options.Value.SubstitutionFormat;

        public void Apply(
            SwaggerDocument swaggerDoc,
            DocumentFilterContext context)
        {
            var versionSegment = DefaultApiVersion.ToString(ApiVersionFormat);

            foreach (var apiDescription in context.ApiDescriptions)
            {
                //If the version is default remove paths like: api/v1.0/controller
                if (apiDescription.GetApiVersion() == DefaultApiVersion)
                {
                    if (apiDescription.RelativePath.Contains(versionSegment))
                    {
                        var path = "/" + apiDescription.RelativePath;
                        swaggerDoc.Paths.Remove(path);
                    }
                }
                //If the version is not default remove paths like api/controller
                else
                {
                    var match = Regex.Match(apiDescription.RelativePath, @"^api\/v\d+");

                    if (!match.Success)
                    {
                        var path = "/" + apiDescription.RelativePath;
                        swaggerDoc.Paths.Remove(path);
                    }
                }
            }
        }
    }
}
Was this page helpful?
0 / 5 - 0 ratings