Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Injecting ApiVersion as a parameter in a controller action method. #922

Discussion options

I'm currently migrating from Microsoft.AspnetCore.Versioning 5.0.0 to Asp.Versioning 7.0.0-preview.2

In the Index method of my HomeController I have an ApiVersion parameter that worked in the old version, but cannot be bound in the new version.
I'm using it to generate links in the response that point to the appropriate controllers which match the version supplied by the user.

This is my ConfigureServices method in Startup

//...
            services
                .AddApiVersioning(options =>
                {
                    options.AssumeDefaultVersionWhenUnspecified = true;
                    options.ReportApiVersions = true;

                    // Default version
                    var defaultVersion = Configuration.GetValue("RESTworld:Versioning:DefaultVersion", "*");
                    if (defaultVersion == "*")
                    {
                        options.ApiVersionSelector = new LatestApiVersionSelector();
                    }
                    else
                    {
                        if (!ApiVersionParser.Default.TryParse(defaultVersion, out var parsedVersion) || parsedVersion is null)
                            throw new ArgumentOutOfRangeException("RESTworld:Versioning:DefaultVersion", defaultVersion, "The setting for \"RESTworld:Versioning:DefaultVersion\" was neither \"*\" nor a valid API version.");

                        options.DefaultApiVersion = parsedVersion;
                    }

                    // Version parameter
                    var allowQueryStringVersioning = Configuration.GetValue("RESTworld:Versioning:AllowQueryParameterVersioning", false);
                    if (allowQueryStringVersioning)
                    {
                        options.ApiVersionReader = ApiVersionReader.Combine(
                            new MediaTypeApiVersionReader(versionParameterName),
                            new QueryStringApiVersionReader(versionParameterName));
                    }
                    else
                    {
                        options.ApiVersionReader = new MediaTypeApiVersionReader(versionParameterName);
                    }
                })
                .AddApiExplorer(options =>
                {
                    options.GroupNameFormat = "'v'VVV";

                    // Do not advertise versioning through query parameter as this is only intended for legacy clients and should not be visible as it is not considered RESTfull.
                    if (options.ApiVersionParameterSource is not MediaTypeApiVersionReader)
                    {
                        options.ApiVersionParameterSource = new MediaTypeApiVersionReader(versionParameterName);
                    }
                })
                .AddMvc();
//...
    public class LatestApiVersionSelector : IApiVersionSelector
    {
        /// <inheritdoc/>
        public ApiVersion SelectVersion(HttpRequest request, ApiVersionModel model)
            => model.ImplementedApiVersions.OrderByDescending(v => v).FirstOrDefault() ?? ApiVersion.Default ?? ApiVersion.Neutral;
    }
[ApiController]
[Produces("application/hal+json")]
[Route("")]
[ApiVersionNeutral]
public class HomeController : ControllerBase
{
    [HttpGet]
    [ProducesResponseType(typeof(Resource), StatusCodes.Status200OK)]
    public virtual IActionResult Index(ApiVersion version)
    {
        // ...
    }
}
You must be logged in to vote

I still think this looks to be a strange setup, but I believe I was able to reproduce your scenario. Some of the core routing setup had to change starting in 6.0. There have been a couple edge case breaking behaviors that I haven't caught, but - in general - it's been for the better.

One of these edge cases is a version-neutral controller. It is necessary to have an API version to build out the route tree. Didn't realize there is a scenario where node could be built without all of the discovered endpoints. This leads to incomplete collation and an incomplete set of API versions, which can produce the behavior you're seeing. Version-neutral endpoints have a special mapping to ApiVersion.Ne…

Replies: 2 comments · 7 replies

Comment options

By not bound, I presume you mean that the action is being reached, but the version parameter is null? This is a bit of a strange setup, but it should work. I think I can create a repro, but you have something handy, it would speed things up.

You must be logged in to vote
0 replies
Comment options

I still think this looks to be a strange setup, but I believe I was able to reproduce your scenario. Some of the core routing setup had to change starting in 6.0. There have been a couple edge case breaking behaviors that I haven't caught, but - in general - it's been for the better.

One of these edge cases is a version-neutral controller. It is necessary to have an API version to build out the route tree. Didn't realize there is a scenario where node could be built without all of the discovered endpoints. This leads to incomplete collation and an incomplete set of API versions, which can produce the behavior you're seeing. Version-neutral endpoints have a special mapping to ApiVersion.Neutral, which is what is used when no version is specified.

A version-neutral endpoint can accept an API version, which appears to be what you expect. This is supposed to work, but since the collation is incomplete, not all of the versions are present. I have a pending fix and it will go into the final release (as well as 6.x). An important difference from previous implementations is that you can no longer use just any ol' API version for neutral endpoint. It must version defined somewhere in the application (ultimately because it's needed for routing).

I've seen people make mistakes in the past with version-neutral endpoints that accept a perfectly valid, but nonexistent version on a neutral endpoint. It was difficult to detect and track down. While the new behavior might be limiting, it's also more sane and likely what people want.

Look for the fix soon.

You must be logged in to vote
7 replies
@wertzui
Comment options

I'm still getting an error with the 7.0.0-rc.1 version.

[15:13:45 INF] Request starting HTTP/2 GET https://localhost:5432/ - -
[15:13:45 DBG] 1 candidate(s) found for the request path '/'
[15:13:45 DBG] Request matched endpoint '400 Invalid API Version'
[15:13:45 DBG] Static files was skipped as the request already matched an endpoint.
[15:13:45 INF] Executing endpoint '400 Invalid API Version'
[15:13:45 INF] Request contained the API version 'b3', which is not valid
[15:13:45 INF] Executed endpoint '400 Invalid API Version'
[15:13:45 INF] Request finished HTTP/2 GET https://localhost:5432/ - - - 400 0 - 19.8483ms
@commonsensesoftware
Comment options

Hmmm... I'm not sure i have enough information to understand where b3 is coming from. That definitely would not be valid. A repro would be best, but the full HTTP request might be enough.

@wertzui
Comment options

I found out, where b3 is coming from:

Chromium (Chrome, Edge) is sending the following Accept header:

text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

The part application/signed-exchange;v=b3 is required by the application to take part in "Signed HTTP Exchanges" defined here https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#section-8.6

It looks as if starting from a certain version, all Chromium Browsers are sending that header automatically.
I removed that header and then it worked.

Is there a possibility to configure that unknown versions should be treated as ApiVersion.Neutral or still hit the defined IApiVersionSelector?

@commonsensesoftware
Comment options

When versioning by media type, the MediaTypeApiVersionReader uses a default parameter name of v but that isn't required. There are several options:

  1. Use a different parameter name for versioning that will not conflict; for example, application/json; api-version=1.0
    a. Configure this via new MediaTypeApiVersionReader( "api-version" ) or whatever name you want
  2. Use the new MediaTypeApiVersionReaderBuilder to include or exclude media types
  3. Extend MediaTypeApiVersionReader or roll your IApiVersionReader

Someone else recently just ran into this exact same issue with this header. The new MediaTypeApiVersionReaderBuilder provides a couple of ways you can deal with it.

Mutually Inclusive

var reader = new MediaTypeApiVersionReaderBuilder()
    .Parameter( "v" )
    .Include( "application/json" ) // other media types are ignored
    .Build();

Mutually Exclusive

var reader = new MediaTypeApiVersionReaderBuilder()
    .Parameter( "v" )
    .Exclude( "application/signed-exchange" ) // ignore this media type 
    .Build();

The MediaTypeApiVersionReader doesn't know or care about a specific media type so that it can work with any of them.

@wertzui
Comment options

The "Mutually Exclusive" way was exactly what I needed.

Thanks!

Answer selected by wertzui
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
🙏
Q&A
Labels
None yet
2 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.