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

Multiple [MapToApiVerion] attributes on a route make the method appear multiple times in the generated docs

Open simple-nathan opened this issue 3 years ago • 7 comments
trafficstars

Rather than copy/paste a whole bunch of code in our API we are trying to add 2 [MapToApiVersion()] attributes and then get the version number from the request HttpContext.GetRequestedApiVersion(). This all seems to work fine except the method appears multiple times in the generated documentation. So if we have v2 and v3, then they both appear in the v2 docs and also in the v3 docs. Is what we are trying to do feasible?

simple-nathan avatar Jun 20 '22 19:06 simple-nathan

Let me try to confirm what you want to achieve. It's not 100% clear which platform this is, but it looks like it might be Web API.

Consider the following:

[ApiVersion("1.0"), ApiVersion("2.0")]
[RoutePrefix("values")]
public class ValuesController : ApiController
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public IHttpActionResult GetV1(ApiVersion version) => Ok(new {message = $"Hello from {version}"});

    [HttpGet]
    [MapToApiVersion("2.0")]
    public IHttpActionResult GetV2(ApiVersion version) => Ok(new {message = $"Hello from {version}"});
}

This should produce the URL http://localhost/values, which when invoked with ?api-version=1.0 goes to GetV1 and ?api-version=2.0 goes to GetV2. There should only be a single entry documented in each API version.

commonsensesoftware avatar Jun 20 '22 20:06 commonsensesoftware

Yes sorry, I should have been clearer. This is a Web API in .NET 6. We are trying to do something like this:

        [HttpGet("test")]
        [MapToApiVersion("2")]
        [MapToApiVersion("3")]
        public async Task<ActionResult> TestRoute(int uid)
        {
            var version = HttpContext.GetRequestedApiVersion().MajorVersion;

            if (version == 2)
            {
                // do something
            }
            else if (version == 3)
            {
                // do something else very similar that stops a large chunk of duplicated code
            }

            return Ok();
        }

The end result of this is that TestRoute V3 action will appear in the V2 docs, and vice versa. What we were hoping would happen is that the action would appear in each docs once only.

simple-nathan avatar Jun 21 '22 13:06 simple-nathan

@simple-nathan, I might be misunderstanding, but IMO, your pattern is actually the issue. You should not need to worry about handling the logic of versioning based on what is coming in, that is done by providing an endpoint which supports one or more versions and the versioning itself is handled via this library.

In your case I would have two endpoints, version 2 and 3, then abstract the logic you want to implement based on the versions into different classes which handle that. This way, v2 can call a method that does whatever and v3 would call the same method, then a series of others potentially, to enrich before sending the reaponse.

As I said though, I might not be getting the whole picture of what you are trying to do and why.

Mhirji avatar Jun 21 '22 17:06 Mhirji

There are numerous ways to divide and conquer how different API versions are implemented. They can be separate types, methods, or even branches within the same method. The approach you chose is at your sole discretion. There are recommendations, but you are in the driver's seat. There are reasons why you might want to interleave within a class or method.

Let's consider a full implementation:

[ApiVersion( "2.0" )]
[ApiVersion( "3.0" )]
[Route( "[controller]" )]
public class ReproController : ControllerBase
{
    [HttpGet( "test/{uid}" )]
    [MapToApiVersion( "2" )]
    [MapToApiVersion( "3" )]
    public IActionResult TestRoute( int uid, ApiVersion version ) =>
        version.MajorVersion switch
        {
            2 or 3 => Ok( new { message = $"Test using v{version}" } ),
            _ => StatusCode( StatusCodes.Status501NotImplemented ),
        };
}

Sidebar: You can use HttpContext.GetRequestedApiVersion(), but allowing Model Binding to provide the ApiVersion via an action parameter is more succinct. This parameter is considered special, much like how CancellationToken works.

This configuration will produce the following results:

URL API Version Method
/repro/test/{uid} 2.0 TestRoute
/repro/test/{uid} 3.0 TestRoute

This is exactly what I would expect to have happen. The API contract says that you have an API that is supported in 2.0 and 3.0. The fact that they are backed a single method is an implementation detail that isn't known to the outside world.

Conversely, if you wanted symmetrical versioning, you could use this same approach even if there were no differences in the implementation. A client would never know that implementation hasn't been changed. On a similar note, implementation details or behaviors that do not change the public facing API contract don't need to be versioned. API Versioning is about versioning the API, not the code. 😉

commonsensesoftware avatar Jun 21 '22 18:06 commonsensesoftware

So in your example, if I went to the docs URL for v2, e.g. http://mydocs.org/v2/ I would only see a single entry for TestRoute ? Or your docs URL would not contain the API version number?

simple-nathan avatar Jun 23 '22 18:06 simple-nathan

By docs, I presume you mean OpenAPI (formerly Swagger). Assuming that is true, then the answer is - yes, provided you're also using the API Versioning API Explorer extensions. The API Explorer extensions collate APIs by their versions. As shown in the example code, this bucketizes each API per group name which is the formatted API version value (ex: v2). In fact, you pretty much have to do it that way because OpenAPI doesn't allow duplicate path entries in a single doc. The only way I know around that is to version by URL segment (ex: /v1, /v2, etc), which I strongly discourage. Even if you happen to version that way, the APIs are still collated by their versions. You'd need some custom code to make it one document, but that doesn't sound like what you want.

If you're using Swashbuckle, you can expected a single entry for the same API in two different documents and versions:

  • /swagger/v2/swagger.json (displayed as V2)
    • /repro/test/{uid} (maps to TestRoute)
  • /swagger/v3/swagger.json (displayed as V3)
    • /repro/test/{uid} (maps to TestRoute)

commonsensesoftware avatar Jun 23 '22 20:06 commonsensesoftware

@simple-nathan were you able to solve your problem? While it's true that might have 2 entries for v2 and v3 that map to the same implementation method, it's just that - an implementation detail. The client/consumer of your API doesn't know they are mapped to same code. As the server, it's your prerogative to change that however you feel as long as you don't break the contract. Two APIs might look identical to the client, but actually have different implementations. At the end of the day, you are documenting your API, not the code behind it. If you end up with identical entries in v2 and v3, that's ok and it doesn't mean that are actually the same.

commonsensesoftware avatar Aug 24 '22 03:08 commonsensesoftware

This question appears to been answered and I presume resolved so I'm going to close it out. If it's not or you still need additional assistance, I'm happy to provide more guidance or reopen the issue. Thanks.

commonsensesoftware avatar Sep 29 '22 14:09 commonsensesoftware