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

Controllers marked [ApiVersionNeutral] do not appear in Swagger #118

Closed
chippiearnold opened this issue Apr 27, 2017 · 26 comments
Closed

Controllers marked [ApiVersionNeutral] do not appear in Swagger #118

chippiearnold opened this issue Apr 27, 2017 · 26 comments

Comments

@chippiearnold
Copy link

I have integrated Swagger to my versioned API successfully and have been playing with creating different versions of controllers. I wanted to mark a particular controller as ApiVersionNeutral, which works fine from my test client, but the controller then disappears from Swagger - understandable, as I can request the version in Swagger and so it makes sense that something without a version doesn't appear.

So, my question is - is there any way to make neutral controllers appear regardless of what version has been requested through Swagger?

@chippiearnold
Copy link
Author

Never mind, I think I have worked this out - I need to amend the way my RoutePrefix works, as I currently have the same prefix across different controllers which is a mistake. I will close this issue as I believe this to be user error - sorry!

@commonsensesoftware
Copy link
Collaborator

You didn't specify if this was ASP.NET Web API or Core. I did a sanity check and it appears that the Web API version does have a bug that doesn't honor API version neutrality.

One thing that can be confusing about API versioning is that once you enable it, everything has an API version. There is really no such thing as no API version. Despite my effort to use a clear name, I failed. API version-neutral does not mean unversioned. Instead, it means every version (e.g. neutral or I don't care which version). This should mean that a version-neutral API should appear for every known version.

@chippiearnold
Copy link
Author

chippiearnold commented Apr 27, 2017

I was just about to re-open, as what I thought I had done wrong hasn't in fact fixed the issue!
To clarify, I'm using ASP.NET Web API (not Core).

To clarify what I'm trying to achieve:

I have a controller called AuditController, which now has a V2 method.
There is another controller called EntityTypesController. Nothing in this has changed with the new version, so I can just append ApiVersion("2.0") and this works fine.

I then considered that, rather than having to append another ApiVersion attribute every time a new version of some other controller is released, if nothing has changed in EntityTypesController then I could simply use [ApiVersionNeutral] - and keep using this until the point at which I need to version the EntityTypesController.

It works fine when I tested this from my test client consuming the API, but I couldn't then see the controller in the Swagger documents - which may be the bug you talk about above.

Hope that adds some clarification!

@chippiearnold
Copy link
Author

chippiearnold commented Apr 27, 2017

Just as a further explanation - I'm trying to avoid my clients needing to pass different versions depending on what service call they are making. This is a gateway api, and I essentially want the client to be able to just specify what "version of the API" they are using once, for every request.

In other words, if a client says they are on version 2 and wants audits, they'll get the V2 audit object. But if they then request EntityTypes, event though they will still pass api-version 2 in the request, they will just get the "VersionNeutral" entity types controller - until such point that I have to amend the EntityTypesController, in which case I will remove the ApiVersionNeutral attribute and replace it with specific ApiVersion attributes instead.

I hope I'm making sense(!)

@commonsensesoftware
Copy link
Collaborator

For documentation, it's definitely an issue. It was a simple oversight on my part. I'm in the process of working on a fix.

In terms of your approach to address your versioning scenarios, this is a plausible solution. Kind in mind, however, that by going version-neutral the controller will accept any version, even ones that that you don't necessarily want to support. This might be fine now, but it could come back to haunt you. I had that happen with one team. They went from being version-neutral to versioned, which one would think should be ok if the client is passing a version. In that case, the clients were, in fact, passing an API version, but it wasn't a supported API version. It was some arbitrary API version that was being accepted and ultimately broke once a specific API version was required. Be careful with being version-neutral if you think you might ever become versioned - there be dragons.

There are some other alternatives to achieving your goal:

  • Use a HttpMessageHandler and set the API version for that specific route very early in the pipeline a la the available, but purposely hidden HttpRequestMessage.SetRequestedApiVersion
  • Enable no version to be specified via the AssumeDefaultVersionWhenUnspecified option, create a custom IApiVersionSelector, and hook in up in the API versioning options

The IApiVersionSelector was specifically meant to help address the scenario you are describing. The single method SelectVersion accepts the current HttpRequestMessage and an aggregation of all discovered API versions (so you don't have to do the hard work). You can then choose the best API version given the API version model or some other information from the current request. In your case, it would be the request itself for one specific route. The IApiVersionSelector.SelectVersion method is only called when no API version is specified and implicitly API versioning is allowed.

This approach can be taken to the extreme, which would be to create a version mapping between clients and implemented API versions. Once a client calls for the first time, they are bound to the API version they matched to. From that point forward, they will always get that version. You'd have to write some bits to store this mapping and you'd want to pre-load it some where in the pipeline, but it is possible. If you have external storage holding this mapping, you'll definitely want to do it outside the IApiVersionSelector because it's design is [intentionally] not asynchronous.

If this is something you are interested in pursuing, you can find more information in the API Version Selector topic. Their implementations are pretty simple. Feel free to checkout the source for how to implement your own.

I hope that helps.

@chippiearnold
Copy link
Author

Wow that is super helpful, thank you - I will look further into the IApiVersionSelector as you suggest. This is a fairly new api and not yet fully out in the wild, so it will be good to get this right up front.

Thanks again!

@emanuel-v-r
Copy link

Hi guys,

Can I find any example on how to expose an ApiVersionNeutral Controller in Swagger?
Sorry guys if this is not the proper place to ask.

Thank you

@commonsensesoftware
Copy link
Collaborator

An API version-neutral controller will appear in every Swagger document (which is per API version). I'm not sure if there is an example for vanilla MVC, but there is an example for OData. You can see the Web API example here, but there's a similar equivalent for Core too.

There is a curious case of all your controllers being version-neutral (which sounds like a really, really bad idea). In this case, you should only have a single API version, which would be defined by ApiVersioningOptions.DefaultApiVersion. All of the controllers would be in this version.

If this is somehow not the case for you, but you have repro you can share (best) or at least your setup, then some better guidance can be provided.

@emanuel-v-r
Copy link

emanuel-v-r commented Oct 9, 2019

An API version-neutral controller will appear in every Swagger document (which is per API version). I'm not sure if there is an example for vanilla MVC, but there is an example for OData. You can see the Web API example here, but there's a similar equivalent for Core too.

There is a curious case of all your controllers being version-neutral (which sounds like a really, really bad idea). In this case, you should only have a single API version, which would be defined by ApiVersioningOptions.DefaultApiVersion. All of the controllers would be in this version.

If this is somehow not the case for you, but you have repro you can share (best) or at least your setup, then some better guidance can be provided.

Hi Chris,

Thank you so much for your fast reply.
For some reason the controller does not appear in swagger.
My setup is the following https://codeshare.io/5DMNOr .
The goal is to have the version 1 and 2, and have some methods that does not depend on the version.
I am using the following attributes in my controllers:
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[RoutePrefix("api{version:apiVersion}/{controller}")]

[ApiVersionNeutral]
[RoutePrefix("api/{controller}")]

Thanks for the help!

@commonsensesoftware
Copy link
Collaborator

Does accessing your API even work? I would not expect it to with this configuration. A controller cannot be simultaneously versioned and version-neutral. This would be ambiguous and should result in a 500.

You should now be able to version down to the action level. This would not be ambiguous because the action level versioning is explicit and trumps whatever is defined at the controller level. The most obvious case for this would be DELETE, which is unlikely to ever change across API versions.

I would start with those modifications and I'd also verify you can even call the API (via Postman or something), if you haven't already. If things still aren't working, then it's possible there is an issue with this particular setup that hasn't been caught before - the test matrix of combinations is quite large. Versioning, and hence API exploration, down to the action level is supposed be supported, even in Web API.

Let me know.

@emanuel-v-r
Copy link

emanuel-v-r commented Oct 9, 2019

not

Sorry I didn't explain myself right. I wanted to say that I use those attributes but in different controllers.
I have the following folders:
Controllers - Here I use [ApiVersionNeutral] for each controller
V1/Controllers - Here I use [ApiVersion("1.0")] for each controller
V2/Controllers - Here I use [ApiVersion("2.0")] for each controller

Detected the problem now, I had a typo in the controller suffix name (blindly I didn't detect it) and that's why it was not appearing in swagger, it had nothing to do with ApiVersionNeutral.
Sorry for the inconvenient.
One more question, is it possible to place the controller swagger doc in some separated section instead of appearing in every version?
Thank you.

@commonsensesoftware
Copy link
Collaborator

Ah … awesome. Glad you got it working.

By separate section, do you mean place all version-neutral APIs in a single document? If yes, that is technically possible, but not supported out-of-the-box. You'd have to extend the VersionedApiExplorer and recollate the results before the Swagger generator uses the discovered action descriptors or provide a Swagger generator extension/customization that recollates the results from the IApiExplorer before generating documents. In Web API and Swashbuckle, you might be able to achieve this via the MultipleApiVersions callback.

Remember that a version-neutral API accepts any and all API versions, including none at all. Since the Swagger documents are generated according to API version, there isn't a nice way to have these tallied together. Furthermore, Swagger/OpenAPI documents generated this way are meant to describe the API surface area by API version. A consumer is looking at all the APIs in a particular version. A version-neutral API should, therefore, appear in every document because it applies to every API version.

While it should be possible to bucketize things the way you want, do keep in mind that a client will have to stitch the set of versioned and version-neutral APIs together. The default behavior collates all applicable APIs together per API version. Also keep in mind that a version-neutral API is orthogonal to a client - e.g. it's more of an implementation detail. Unless you allow the client to not specify an API version (which you should never allow), they don't know that behind the scenes their request is going to the same implementation; regardless of the API version they specify. Finally, when it comes to a Swagger/OpenAPI document, a version-neutral API still requires an API version by default. By putting things into their own bucket, you are inviting clients to specify bogus API versions that would likely otherwise be avoided. This could lead to problems down the road. Keep in mind that when a client sees a version-neutral API in the Swagger document, they don't know that it's version-neutral; they just know it appears in every document and API version.

I hope that helps.

@emanuel-v-r
Copy link

Ah … awesome. Glad you got it working.

By separate section, do you mean place all version-neutral APIs in a single document? If yes, that is technically possible, but not supported out-of-the-box. You'd have to extend the VersionedApiExplorer and recollate the results before the Swagger generator uses the discovered action descriptors or provide a Swagger generator extension/customization that recollates the results from the IApiExplorer before generating documents. In Web API and Swashbuckle, you might be able to achieve this via the MultipleApiVersions callback.

Remember that a version-neutral API accepts any and all API versions, including none at all. Since the Swagger documents are generated according to API version, there isn't a nice way to have these tallied together. Furthermore, Swagger/OpenAPI documents generated this way are meant to describe the API surface area by API version. A consumer is looking at all the APIs in a particular version. A version-neutral API should, therefore, appear in every document because it applies to every API version.

While it should be possible to bucketize things the way you want, do keep in mind that a client will have to stitch the set of versioned and version-neutral APIs together. The default behavior collates all applicable APIs together per API version. Also keep in mind that a version-neutral API is orthogonal to a client - e.g. it's more of an implementation detail. Unless you allow the client to not specify an API version (which you should never allow), they don't know that behind the scenes their request is going to the same implementation; regardless of the API version they specify. Finally, when it comes to a Swagger/OpenAPI document, a version-neutral API still requires an API version by default. By putting things into their own bucket, you are inviting clients to specify bogus API versions that would likely otherwise be avoided. This could lead to problems down the road. Keep in mind that when a client sees a version-neutral API in the Swagger document, they don't know that it's version-neutral; they just know it appears in every document and API version.

I hope that helps.

Thank you so much for your time, very helpful explanation.
In this case I do allow to not specify version since I am using ApiVersionNeutral for more generic endpoints like HealthCheck, not sure if it is worth to implement those changes though.

@nathanjwtx
Copy link

Sorry to bump an old thread. Is there any additional setup required to get controllers marked as ApiVersionNeutral to show up in Swagger? I can see that route is working regardless of version but the route is not showing up in my documentation :/

@commonsensesoftware
Copy link
Collaborator

@nathanjwtx do you have any controllers with a specific API version? A version-neutral API should appear in every group by API version. The API Explorer extensions, literally just add a copy to every group. However, if there are zero, specific API versions, then there is no group to put them in! That is an edge that has no real defense because it's a non sequitur in the context of API versioning. It should only be a problem if every API is version-neutral, but then you have to ask why you have versioning in the first place then?

A version-neutral API accepts any and all API versions, including none at all. This is largely an implementation detail and one that cannot be easily conveyed in OpenAPI. Version-neutral is not the same thing as no API version (though it's kind of easy to think about it that way). You shouldn't want to have APIs specifically show up as version-neutral in the OpenAPI UI, but you could make it work with some effort on your part. The default behavior simply adds version-neutral APIs to every known API version. A client has no idea that it's actually version-neutral. The other benefit of that is that it helps clients avoid calling your API with invalid API versions against a version-neutral API (which I've seen in the wild).

I'm not sure if that is your current state of affairs, but that is the only scenario I know of where this can happen. If that is your scenario, you should see that adding any API with a specific API version will make it appear in the API Explorer. If that isn't your case, then you'll have to share some additional details.

@nathanjwtx
Copy link

Yes, at the moment all of our routes are either v1 or v2. Maybe a bit about the problem I'm trying to overcome. We have a couple of routes that aren't changing with the new version so I tried adding [MapToApiVersion("1")] and [MapToApiVersion("2")] to the route. But that the made the route appear twice in each versions docs. So [ApiVersionNeutral] appears to be the solution.

@commonsensesoftware
Copy link
Collaborator

If you can provide a sample, I can provide more concrete guidance. [MapToApiVersion] will not work with [ApiVersionNeutral]. While it seems like it should or, at least, be a nice convenience, it won't work. I've explored it before. The main reason it isn't supported is that what should happen when there is no action to map the route to? By definition of the policies, the route and API version (present or no) matched, but there is no action left to dispatch to. Essentially, by claiming to be version-neutral, but then not having a mapped action violates the established contract/semantics, even though 400 or 404 could be returned.

I'm getting the impression that you are trying to minimize touch points between versions or you are bolting them on formally with API Versioning. Admirable goals, but punting to [ApiVersionNeutral] is not the thing that will help you. With a little more context or an example, I can elaborate. [ApiVersionNeutral] is meant for something that doesn't change across API versions; for example, the ubiquitous /ping route or something like DELETE /resource/{id}.

@nathanjwtx
Copy link

We have a status route that won't be changing across versions as all it does it query the status of internal APIs that feed the public API. Here is a stripped down version of the controller. With the ApiVersion lines uncommented, the route appears twice in each set of documentation. With those lines commented, and ApiVersionNeutral added, the route doesn't appear at all.

    [Route("[controller]")]
    [ApiController]
    // [ApiVersion("1")]
    // [ApiVersion("2")]
    [ApiVersionNeutral]
    public class StatusController : InternalApiController
    {
        public readonly IStatusService _statusSvc;

        public StatusController(IStatusService statusSvc)
        {
            _statusSvc = statusSvc;
        }

        // GET: /Status/
        /// <summary>
        /// Get API system status.
        /// </summary>
        /// <remarks>
        /// Get API system status.
        /// </remarks>
        /// <returns>A JSON object containing details about the system status.</returns>
        [HttpGet]
        // [MapToApiVersion("1")]
        // [MapToApiVersion("2")]
        [Produces("application/json")]
        [AllowAnonymous]
        public async Task<ActionResult<StatusResult>> Get(CancellationToken cancellationToken = default)
        {
            var result = await internal_service_name();

            return StatusCode(StatusCodes.Status200OK, new StatusResultV2(result));
        }

@commonsensesoftware
Copy link
Collaborator

@nathanjwtx You cannot apply [ApiVersion] and [ApiVersionNeutral] at the same time. You shouldn't be able to have the API Explorer extensions produce duplicates this way - but maybe. [ApiVersionNeutral] takes precedence; once observed all actions by controller or specific actions are version-neutral forevermore. If the API Explorer somehow produces such a result, it will definitely fail at runtime with a 500 (due to ambiguous routes).

The question is whether you have [ApiVersion] applied to any other controller; you need at least one. I assume so, but double-checking. I took the provided OpenAPI example project with Swashbuckle and added the following (per your example):

/// <summary>
/// Provides status information.
/// </summary>
[ApiController]
[ApiVersionNeutral]
[Route( "[controller]" )]
public class StatusController : ControllerBase
{
    /// <summary>
    /// Returns API status information.
    /// </summary>
    /// <returns>The current API status.</returns>
    [HttpGet]
    public IActionResult Get() => Ok();
}
version-neutral-0 9 version-neutral-1 0

Without going through every supported version, you can see that version-neutral will show up in each defined version. If there are zero explicit versions defined, then there is nowhere for them to show up. Comparing against the sample project might help you determine what's different in your own project.

BTW: You don't need to use [MapToApiVersion] if you aren't interleaving. This might have just been part of the experiments in shuffling things around. Each action on a controller implicitly maps to all the versions defined by their containing controller.

@nathanjwtx
Copy link

We have [ApiVersion] applied to virtually all other routes as most are changing between v1 and v2.

Would this have something to do with how we setup our versioning? We went with adding the v2 routes in the same controller as the v1, but with a new name, e.g. GetV2.

@commonsensesoftware
Copy link
Collaborator

Hmm... names such as GetV2 are irrelevant. That is an implementation detail that isn't used for any part of the documentation.

I whipped up an example that should repro your scenario:

/// <summary>
/// Represents speech.
/// </summary>
/// <param name="Message">The spoken message.</param>
public record Speech( string Message );

/// <summary>
/// Manages spoken words.
/// </summary>
[ApiController]
[ApiVersion( "1.0" )]
[ApiVersion( "2.0" )]
[Route( "[controller]" )]
public class SpeechController : ControllerBase
{
    /// <summary>
    /// Gets a default spoken message.
    /// </summary>
    /// <returns>A spoken message.</returns>
    [HttpGet]
    [MapToApiVersion( "1.0" )]
    public IActionResult Get() => Ok( new Speech( "Hello world!" ) );

    /// <summary>
    /// Gets a default spoken message.
    /// </summary>
    /// <param name="version">The requested API version.</param>
    /// <returns>A spoken message.</returns>
    [HttpGet]
    [MapToApiVersion( "2.0" )]
    public IActionResult GetV2( ApiVersion version ) =>
        Ok( new Speech( $"Hello world! ({version})" ) );

    /// <summary>
    /// Echoes the spoken speech.
    /// </summary>
    /// <param name="speech">A spoken message.</param>
    /// <returns>The original, spoken message.</returns>
    [HttpPost]
    [MapToApiVersion( "2.0" )]
    public IActionResult Echo( [FromBody] Speech speech ) => Ok( speech );
}

Which produces what I would expect for v1 versus v2 which also includes a version-neutral API:

speech-v1 speech-v2

Attached is the ever-so-slightly changed Swashbuckle Sample which illustrates everything all put together. Maybe that will help you figure out what's different in your application.

@nathanjwtx
Copy link

I haven't had time to fully try this out yet but to clarify, you can't combine ApiVersionNeurtral routes with versioned routes in the same class?

@commonsensesoftware
Copy link
Collaborator

@nathanjwtx to clarify, you cannot have a route that is both versioned and version-neutral; regardless of whether it is the same class. The result is ambiguous. Declaring an API version-neutral is an all or nothing definition. Mapping matched versions to one place and all unmatched versions to another place is unsupported. It is effectively the same problem as combining [MapToApiVersion] with [ApiVersionNeutral]. If there is a matching route, a version-neutral route can no longer fail based on an unmatched action (e.g. method). It's a violation of how it is meant to work.

If you really want a version-neutral endpoint, but with version-specific logic, this is possible, but it's not afforded by the routing system. In such a scenario, your API is version-neutral and your action can consume the ApiVersion via model binding (it is special; similar to CancellationToken) to make version-specific decisions. You can also get the value from the HttpContext.GetRequestedApiVersion() extension method. You'll have to decide what it means if no API version is provided.

@nathanjwtx
Copy link

Sorry, I didn't explain what I meant very well. In my class can I have the following (pseudo code)? Or would the ItemStatus route need to be in its own class?

namespace Items;

[ApiController]
[ApiVersion("1")]
[ApiVersion("2")]
public class ItemsController : ControllerBase
{
[MapToApiVersion("1")]
public int GetItem....

[MapToApiVersion("2")]
public int GetItemV2....

[ApiVersionNeutral]
public string ItemStatus...
}

@commonsensesoftware
Copy link
Collaborator

@nathanjwtx yes - that is a supported scenario. What that means is that the specific, corresponding action is API version-neutral forevermore. You just need to make sure that's what you want. 😉

For example, I can add this additional action the SpeechController above:

/// <summary>
/// Deletes a spoken message.
/// </summary>
/// <returns>None.</returns>
[HttpDelete]
[ApiVersionNeutral]
public IActionResult Delete() => NoContent();

Now, DELETE /speech will show up in all API versions. This a fairly common scenario since DELETE isn't expected to change over time. This was the original intent of how a specific action would be API version-neutral. Given the previously attached sample code, what may not be obvious is that this action will also show up for v0.9 and v3.0, even though the SpeechController only has 1.0 and 2.0. This is by design and congruent with the established rules for being version-neutral.

@nathanjwtx
Copy link

Hmm, I'm stumped. I can make requests to the version neutral route but I can't get it to appear in the generated docs. I looked through the sample project you attached but I couldn't see anything in the Swagger setup that we don't have. As a hacky workaround I've written a v2 route that calls the v1 route. Not ideal but at least now it appears in the docs.
I'll keep playing around with it.
Thanks for your help 👍

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

4 participants