aspnet-api-versioning icon indicating copy to clipboard operation
aspnet-api-versioning copied to clipboard

API Version Neutral Throwing 404 - DotNet 8.0 Upgrade

Open FrankRua opened this issue 11 months ago • 2 comments
trafficstars

Is there an existing issue for this?

  • [x] 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 https://github.com/dotnet/aspnet-api-versioning/issues/1093 do you think you have any insight to this?

FrankRua avatar Dec 03 '24 13:12 FrankRua

+1

david-shanks avatar Dec 03 '24 15:12 david-shanks

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.

commonsensesoftware avatar Dec 03 '24 17:12 commonsensesoftware

Following up. Did the explanation clarify things and resolve the issue? Were you able to find a workaround or solution? I believe this issue has landed on this this is the expected behavior (now), but I wanted to confirm before closing it out.

commonsensesoftware avatar Nov 15 '25 22:11 commonsensesoftware