dotnet / aspnet-api-versioning

Provides a set of libraries which add service API versioning to ASP.NET Web API, OData with ASP.NET Web API, and ASP.NET Core.
MIT License
3.06k stars 704 forks source link

Versioning not working #1112

Closed ilcos91 closed 1 month ago

ilcos91 commented 1 month ago

Is there an existing issue for this?

Describe the bug

I have just moved from Microsoft.AspNetCore.Mvc.Versioning to Asp.Versioning I have defined two versions for the same route. When I try to perform the request I get this error:

Microsoft.AspNetCore.Mvc.Infrastructure.ActionSelector[1]
Request matched multiple actions resulting in ambiguity. Matching actions:...

Before the migration everything was working fine, now we would like to move to the Asp.Versioning library since the old one is deprecated.

Expected Behavior

I would expect that the versioning keep working as before.

Steps To Reproduce

The request:

https://localhost/api/deliveries?v=2

This is how I defined the controller and the two routes:

[Route("api/[controller]")]
[ApiController]
[ApiVersion(1)]
[ApiVersion(2)]
[ApiVersion(3)]
[Authorize(AuthenticationSchemes = "Key")]
public class DeliveriesController : ControllerBase
{

[HttpPost]
[Obsolete("Please use the latest version of this service")]
[MapToApiVersion(1)]
public IActionResult GetDeliveries(
    [FromHeader(Name = "ContextOwner")] string contextOwnerHeaderString,
    [FromHeader(Name = "Company")] string companyHeaderString,
    [FromHeader(Name = "SourceDeviceType")] string sourceDeviceTypeHeaderString,
    [Required][FromBody] DeliveriesFilterObject filters)
{
return Ok();
}

[HttpPost]
[MapToApiVersion(2)]
public IActionResult GetDeliveries_V2(
    [FromHeader(Name = "ContextOwner")] string contextOwnerHeaderString,
    [FromHeader(Name = "Company")] string companyHeaderString,
    [Required][MinLength(2)][FromHeader(Name = "LanguageCode")] string language,
    [Required][FromBody] GenericPaginationFiltersObject<PaginationObject, DeliveriesFiltersV2> filters)
{
return Ok();
}
}

and this is the teh configuration in ConfigureServices in the Startup:

services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new QueryStringApiVersionReader("v");
}).
    AddMvc().
    AddApiExplorer(options =>
    {
        options.GroupNameFormat = "'v'VVV";
        options.SubstituteApiVersionInUrl = true;
    });

Exceptions (if any)

With *** I'll hide private data

Microsoft.AspNetCore.Mvc.Infrastructure.ActionSelector[1]
      Request matched multiple actions resulting in ambiguity. Matching actions: 
***.GetDeliveries
***.GetDeliveries_V2
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HN7A0E490NKF", Request id "0HN7A0E490NKF:00000001": An unhandled exception was thrown by the application.
      Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied:

      ***
      ***
         at Microsoft.AspNetCore.Mvc.Infrastructure.ActionSelector.SelectBestCandidate(RouteContext context, IReadOnlyList`1 candidates)
         at Microsoft.AspNetCore.Mvc.Routing.MvcAttributeRouteHandler.RouteAsync(RouteContext context)
         at Microsoft.AspNetCore.Routing.Tree.TreeRouter.RouteAsync(RouteContext context)
         at Microsoft.AspNetCore.Routing.RouteCollection.RouteAsync(RouteContext context)
         at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
         at ***
      --- End of stack trace from previous location ---
         at ***
         at Microsoft.WebTools.BrowserLink.Net.BrowserLinkMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

.NET Version

.NET 8

Anything else?

No response

commonsensesoftware commented 1 month ago

Thanks for the info and repro setup; it's useful. Taking what you've provided, I added the DeliveriesController to the OpenAPI example project and everything works as expected. The exact code I dropped in was:

[Route( "api/[controller]" )]
[ApiController]
[ApiVersion( 1 )]
[ApiVersion( 2 )]
[ApiVersion( 3 )]
public class DeliveriesController : ControllerBase
{
    [HttpPost]
    [Obsolete( "Please use the latest version of this service" )]
    [MapToApiVersion( 1 )]
    public IActionResult GetDeliveries(
        [FromHeader( Name = "ContextOwner" )] string contextOwnerHeaderString,
        [FromHeader( Name = "Company" )] string companyHeaderString,
        [FromHeader( Name = "SourceDeviceType" )] string sourceDeviceTypeHeaderString) => Ok();

    [HttpPost]
    [MapToApiVersion( 2 )]
    public IActionResult GetDeliveries_V2(
        [FromHeader( Name = "ContextOwner" )] string contextOwnerHeaderString,
        [FromHeader( Name = "Company" )] string companyHeaderString,
        [Required][MinLength( 2 )][FromHeader( Name = "LanguageCode" )] string language) => Ok();
}

This contains a few very minor tweaks to just get it running.

The exception stack trace shows:

...
Microsoft.AspNetCore.Mvc.Infrastructure.ActionSelector.SelectBestCandidate
...

This suggests that you may be still using the legacy routing system backed by IRouter; otherwise, known as convention-based routing. This method of routing was dropped in 6.0 and is called out in the migration guide. This was not a decision made lightly. I would have liked to keep it for edge cases, but it was fraught with problems and painful to maintain the right behavior. If you had stayed on that path with previous versions, you have probably just been lucky enough to never hit any of the sharp edges. There are many however. Aside from Endpoint Routing being the de facto method going forward, route performance will improve. In the legacy routing system, it was possible to re-enter candidate selection with IActionSelector. This was very problematic for API Versioning because that meant it had to hook a catch all route and wait to end after all possible paths had been exhausted to make a decision. If it didn't do that, then routing might short-circuit too early with an error response. The most common cause of this behavior is if you had /deliveries/{id} and /deliveries/{id:int} across versions. This would lead to a second evaluation of candidates.

Solution

Unless I'm otherwise mistaken or missed something, things are not working because you aren't using Endpoint Routing. You must use Endpoint Routing. That is now the only supported routing system.

Observations

ilcos91 commented 1 month ago

Thanks for the suggestion about the languages management, I'll improve it as soon as I can. About the version 3, it is there because I really use it, simply I did not pasted the entire controller since not useful for the purpose. Now the problem is that while I'm trying to enable Endpoint Routing I'm getting this error: Image and I cannot find what I'm missing. Do you have any idea?

ilcos91 commented 1 month ago

After some more tries I got it run, your help definetly solved the problem. To solve the last error I just needed to refactor also the Program.cs file and avoid using the Startup configuration.

Thank you