Skip to content
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

API Version Neutral Throwing 404 - DotNet 8.0 Upgrade #1116

Open
1 task done
FrankRua opened this issue Dec 3, 2024 · 2 comments
Open
1 task done

API Version Neutral Throwing 404 - DotNet 8.0 Upgrade #1116

FrankRua opened this issue Dec 3, 2024 · 2 comments

Comments

@FrankRua
Copy link

FrankRua commented Dec 3, 2024

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

In upgrading applications from DotNet 6.0 to DotNet 8.0 I encountered an issue with routing version neutrality.

My existing implementation has a neutral healthcheck controller that accomplishes the following:

HealthCheck endpoints resolve in swagger for each present version
HealthCheck endpoint is responsive at any agnostic version, or versions not explicitly in use by the application (example/api/v999/healthcheck)
Here is my current structure:

using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;

namespace example.Controller
{
    [ApiVersionNeutral]
    [ApiController]
    [Route("example/api/[controller]/[action]")]
    public class HealthCheckController : ControllerBase
    {
        [HttpGet]
        public IActionResult Heartbeat() => Ok("Beep");
    }
}

In Swagger this appears as:
Image

However, after the upgrade I can no longer target the healthcheck endpoint agnostically. For any version beyond what's in specific use by my application controllers I will receive a 404 not found.

This is an issue for service infrastructure that targets a v1/healthcheck on an application that no longer has v1 controllers.

I was able to overcome this behavior by modifying my route to {version:int}:

    [ApiVersionNeutral]
    [ApiController]
    [Route("example/api/v{version:int}/[controller]/[action]")]
    public class HealthCheckController : ControllerBase
    {
        [HttpGet]
        public IActionResult Heartbeat() => Ok("Beep");
    }

but this is undesirable as it breaks my swagger UI - expecting a parameter:

Image

For reference here is how my application versioning is configured:

        public static void ConfigureApiVersioning(this IServiceCollection services)
        {
            services.AddApiVersioning(
                options =>
                {
                    options.DefaultApiVersion = new ApiVersion(1, 0);
                    options.ReportApiVersions = true;
                    options.AssumeDefaultVersionWhenUnspecified = true;
                    options.ApiVersionReader = new UrlSegmentApiVersionReader();
                })
                .AddApiExplorer(
                options =>
                {
                    options.GroupNameFormat = "'v'VVV";
                    options.SubstituteApiVersionInUrl = true;
                    options.AddApiVersionParametersWhenVersionNeutral = true;
                });
        }

Where there any breaking changes around the ApiVersionNeutral attribute?

Expected Behavior

No response

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

No response

Anything else?

@commonsensesoftware -- I see you commented on a similar issue #1093 do you think you have any insight to this?

@david-shanks
Copy link

+1

@commonsensesoftware
Copy link
Collaborator

You mentioned you're upgrading from .NET 6, but you didn't say which version of API Versioning you were using. Based on the behavior you are describing, I'm guessing you are upgrading from Microsoft.AspNetCore.Mvc.Versioning. That package only ever supported up to .NET 5. 👏🏽 that you got it to work, but it was not officially supported with .NET 6.

Starting in API Versioning 6.0 and the Asp.Versioning.Mvc package, a few things had to change in routing to address other outstanding issues. This is described in the release notes and resurfaced with additional details in the routing behaviors of the migration guide. In short, the API version is resolved much earlier in the request and anchored to endpoints. The consequence of this change to routing is that you can no longer have a version-neutral endpoint for a version that doesn't exist at all. Honestly, this is how it should have been all along. Version-neutral doesn't mean no version, it means any (defined) version or none at all, which matches the implicit, logical default. Endpoints for an version-neutral can only fan out from a defined, collated set of API versions. A similar issue exists for OpenAPI. A version-neutral API doesn't define any versions so it fans out for every defined version. If you remove a version, so too will the version-neutral endpoint be removed.

You specified AssumeDefaultVersionWhenUnspecified = true, but you are versioning by URL segment. That's probably not going to do what you think it will do. It's not possible to have a default value in the middle of a route template. order/{id}/items will also not work without a value for {id}. This one of the many problems and consequences of routing by a URL segment and highlights how it is not RESTful (as it violates the Uniform Interface constraint).

The best choice would be to simply have your route template be "example/api/[controller]/[action]". What's the point of specifying a version for an endpoint that isn't versioned? You could also just hard code it to "example/api/v1/[controller]/[action]". This would solve most of your issues for routing and OpenAPI. The one thing that you lose is that example/api/v1.0/[controller]/[action] will no longer work. I suspect most clients do not specify 1.0 so that's probably a non-issue.

It's not entirely clear me why a version-neutral endpoint should exist for an API version that doesn't exist anywhere else. That suggests that it's not actually version-neutral and instead just matches anything. An API, even a version-neutral one, that accepts any old well-formed API version seems nonsensical. I have witnessed teams run into problems when they realized they were matching client requests with versions specified that don't exist in their API. There be 🐉🐉🐉. An alternate approach is to move away from being version-neutral and instead have your health check support a fixed set of versions. This can be achieved several ways:

  1. Remove [ApiVersionNeutral] and list all of the [ApiVersion(1.0)] and so on
  2. Use a convention for the HealthCheckController. This allows you to define API versions imperatively with code and perhaps configuration
  3. Create and use a custom attribute or convention
    a. You can extended ApiVersionsBaseAttribute or implement IApiVersionProvider directly. You could then have [ApiVersions(1.0, 2.0, 3.0)] and so on.
    b. A custom convention can do something similar. It can be based on configuration, app heuristics, or your own custom attributes

Remember that API versions must be explicit. Version-neutrality conveniently maps to any API version you define and also allows the omission of the API version as this is equivalent to matching an initial, unversioned endpoint. If it was really no versioning, then the attribute would have been called something like [Unversioned]. 😉

Hopefully, this wasn't a soapbox response. If you're using Asp.Versioning.* and see this problem, then there might actually be an issue. I can't recall anything that changed related to routing between 6.0 and 8.0, save refined Minimal API support. Assuming you are actually migrating from the old libraries, this should give you some direction and options.

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

3 participants