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

Advice on integrating swagger via Swashbuckle #46

Closed
davetransom opened this issue Nov 4, 2016 · 10 comments
Closed

Advice on integrating swagger via Swashbuckle #46

davetransom opened this issue Nov 4, 2016 · 10 comments
Assignees
Labels

Comments

@davetransom
Copy link

I'm curious if there has been any attempts at connecting controllers that use this versioning scheme with Swagger/Swashbuckle? I thought I'd ask here since I can see this being used often in conjunction with those kinds of libraries.

I think I've got the version resolution sorted. Does this logic look correct for determining which actions to include per API version? or is there a better way....

static bool ResolveVersionSupportByApiVersionAttributes(ApiDescription apiDescription, string targetApiVersion)
{
  if (apiDescription.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<ApiVersionNeutralAttribute>(true).Any())
    return true; // always include neutrally marked controllers

  var controllerVersionAttributes = apiDescription.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<ApiVersionAttribute>(true);
  if (!controllerVersionAttributes.Any())
    return true; // include when no attributes are defined

  if (targetApiVersion.StartsWith("v"))
    targetApiVersion = targetApiVersion.Substring(1); // remove the leading "v" in `v{x.x}`

  var apiVersion = ApiVersion.Parse(targetApiVersion);

  var controllerApiVersion = controllerVersionAttributes
    .Where(x => x.Versions.Contains(apiVersion))
    .FirstOrDefault();

  // has a compatible version, now check the action for [MapToApiVersion]
  if (controllerApiVersion != null)
  {
    var actionMapToAttributes = apiDescription.ActionDescriptor.GetCustomAttributes<MapToApiVersionAttribute>(true);
    if (!actionMapToAttributes.Any())
      return true; // no MapTo attributes matched, then include the action

    if (actionMapToAttributes.Any(x => x.Versions.Contains(apiVersion)))
      return true; // include mapped action
  }

  return false;
}

but I wanted to get a list of versions discovered in the assembly, rather than manually defining them. Should I also loop over all the ControllerDescriptors here as well, or is there a "discovered versions" registry somewhere (I couldn't find one.)

GlobalConfiguration.Configuration
  .EnableSwagger(c => {
    c.MultipleApiVersions(
      ResolveVersionSupportByApiVersionAttributes,
      (versionInfoBuilder) => {
        
        // unsure how to get all the supported versions that were discovered here
        versionInfoBuilder.Version("2.0", "Swashbuckle Dummy API v2.0");
        versionInfoBuilder.Version("1.0", "Swashbuckle Dummy API v1.0");
        versionInfoBuilder.Version("1.0-alpha", "Swashbuckle Dummy API v1.0-alpha");
        
      }
    );

    c.CustomProvider(defaultProvider => new ODataSwaggerProvider(defaultProvider, c)
      .Configure(odata => odata.IncludeNavigationProperties())
    );
  })
  .EnableSwaggerUi(c => {
    c.EnableDiscoveryUrlSelector();
  });

Appreciate your thoughts here :)

PS: I haven't quite got a working version with config.MapVersionedODataRoutes("odata", "odata", models);, but it works happily if I use the built in config.MapODataServiceRoute("odata", "odata", models.First()) and pass in the first model only.

@commonsensesoftware
Copy link
Collaborator

There have definitely been some thoughts and discussions about lighting that up, but there aren't any concrete plans just yet. It was actually on my mind in the earliest incarnations of this work a couple of years ago before Swagger was all the rave.

With that in mind, it was important to make sure that the API version metadata doesn't solely rely on attributes and can be easily be retrieved by other components such as extension points.

ApiVersionModel serviceApiModel = controllerDescriptor.GetApiVersionModel();
ApiVersionModel actionApiModel = actionDescriptor.GetApiVersionModel();

if ( !serviceModel.IsApiVersionNeutral )
{
    foreach ( var version in serviceApiModel.SupportedApiVersions )
    {
        // TODO:
    }

    foreach ( var version in serviceApiModel.DeprecatedApiVersions )
    {
        // TODO:
    }
}

The collections of API versions have the following meanings:

Collection Meaning
IsApiVersionNeutral The service is API version-neutral; all other collections will be empty
SupportedApiVersions All supported API versions of a service
DeprecatedApiVersions All deprecated API versions of a service
ImplementedApiVersions All supported and deprecated API versions for a service
DeclaredApiVersions The API versions explicitly declared by a specific controller or action; for documentation, this only interesting for filtering and matching actions.

As it happens, the design of ASP.NET Web API never accounted for the fact that there could be multiple implementations of a route. There is handling for duplicate routes by default, but this is assumed to be always be a developer mistake. This made supporting API versioning in Web API a bit of a challenge and sometimes wonky. Fortunately, the model is much better in ASP.NET Core.

As a result of these assumptions, I had to introduce a special HttpControllerDescriptorGroup which aggregates all HttpControllerDescriptor for a service together. If you try to rely solely on the IHttpControllerSelector.GetControllerMapping method, it will make things a bit odd. In order for that to work, all of the similar controllers must have the same key, which there is no guarantee of. In addition, the API version aggregation work is only done at runtime because it's based off of the candidates the routing infrastructure provides. I've made all the related APIs and extensions methods public so you should be able to take advantage of them in one of a couple of ways:

Option 1

var controllerSelector = configuration.Services.GetHttpControllerSelector();
var versionMapping =
    controllerSelector.GetControllerMapping()
                      .ToDictionary(
                        kvp => kvp.Key,
                        kvp =>
                        {
                            // this could be an HttpControllerDescriptorGroup
                            // which is enumerable. account for this without
                            // making a hard dependency on that type
                            var descriptors = kvp.Value as IEnumerable<HttpControllerDescriptor ??
                                              new []{ kvp.Value };
                            return descriptors.Select( d => d.GetApiVersionModel() ).Aggregate();
                        } );

// TODO: use controller name to API version mapping

Option 2

var services = configuration.Services;
var assembliesResolver = services.GetAssembliesResolver();
var typeResolver = services.GetHttpControllerTypeResolver();
var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver );
var controllerDescriptors = controllerTypes.Select( controllerType =>
                                new HttpControllerDescriptor(
                                        configuration,
                                        string.Empty,
                                        controllerType ) );

// TODO: you need a way to logically group services together here.
// there's nothing out-of-the-box yet, but it will likely make sense
// at some point. maybe something like:
//
// [ApiServiceName( "HelloWorld" )]

IDictionary<string, List<HttpControllerDescriptor>> groupedControllerDescriptors = GroupControllersSomehow();
var versionMapping =
    groupedControllerDescriptors.ToDictionary(
                                  kvp => kvp.Key,
                                  kvp => kvp.Value.Select( d => d.GetApiVersionModel() ).Aggregate() );

// TODO: use controller name to API version mapping

Additional Notes

When you inspect the API version model for actions, keep in mind that does not relate to routing or the version of the service. The API version for an action is only used for selecting which method to dispatch to when controllers have interleaved API versions. Don't interleave versions and things are much simpler. If you do want to support it, then just make sure you choose the action that has at least one explicitly matching, mapped API version. Actions cannot be version-neutral nor have deprecated versions. The DeclaredApiVersions property is probably the most useful here. Any action that does not have at least one explicitly mapped API version always implicitly matches the API version of the defining service (kind of like a method overload).

That should be enough to get you started. Let me know if you have more questions. If you come up with something generic, please share.

@davetransom
Copy link
Author

Nice amount of detail there, points me in the right direction. I'm glad I asked :)

Thanks for the response.

@RehanSaeed
Copy link

Is there a sample showing how to get this done? If not, would it be possible to add a really simple one in the project samples?

@RehanSaeed
Copy link

RehanSaeed commented Mar 28, 2017

For anyone else coming here. looks like built in support using ASP.NET Core's abstraction layer is coming soon in #60.

If you need full support today, there is this thread domaindrivendev/Swashbuckle.AspNetCore#244 with a full blown NuGet package you can use called SwashbuckleAspNetVersioningShim.

I think I'll be waiting for proper support.

@commonsensesoftware
Copy link
Collaborator

It is indeed coming. I just updated PR #103 with the alpha support for ASP.NET Core. I think I have most of the right abstractions and setup in place for most scenarios. I have a sample project up so you can see how things will wire up. You'll want to look at Startup.cs and ImplicitApiVersionParameter.cs.

I'm avoiding any direct coupling on Swagger or Swashbuckle, which is why the ImplicitApiVersionParameter.cs is needed (unless I find an alternate way to include it). Fortunately, this class only requires minimal work to implement. It also varies greatly, depending on how you choose to implement API versioning. This might be better handled with a template or directly in Swashbuckle. While the API explorer work is meant to support Swagger and Swashbuckle, it could be used in other contexts as well.

I'm definitely looking for feedback to make sure this is what the community has really been asking for. Thus far, I think it's really close. If you want to try it, feel free to pull the branch down. Thanks.

@Ramya5108
Copy link

Ramya5108 commented Jun 22, 2017

Hi team,

Thanks for the code above on versioning , but i am facing an issue with the below code, please help if i am missing out,

1.The swagger generated doesn't show the any information about the second method on the swagger/docs/v2 generated.
2."version:apiVersion" occurs on the swagger ui :(, while we want it as v1.0 /hello and
v2.0/hello.

[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class SampleController
{
[MapToApiVersion("1.0")]
[Route("version:apiVersion/hello")]
[HttpGet]
public string Get()
{
return "Hellov1";
}
}

    [MapToApiVersion("2.0")]
    [Route("version:apiVersion/hello")]
    [HttpGet]
    public string GetV2()
    {
        return "Hellov2";
    }

}

@commonsensesoftware
Copy link
Collaborator

@Ramya5108 you have not defined the route parameter and constraint correctly. The value must be surrounded with { }. If you want a v in your path, then you will need to add that to the template as well. This is what a well-formed route template should look like: v{version:apiVersion}/hello.

In order to integrate with Swagger/Swashuckle, you'll want the appropriate corresponding API explorer package. The links, by platform, can be found in the API Documentation topic.

It's worth noting that neither the API explorer nor Swashbuckle will translate your route template from v{version:apiVersion}/hello to v1/hello, just as it will not translate hello/{id} to hello/42. However, the API explorer will document the API version parameter for you and give it a default value based on the mapping so that a user doesn't have to input it within the Swagger UI. The name of the API version parameter is derived from the route template parameter (just like any other route template parameter). In this case, the value will be version. You can change it to any other valid name you choose. For example, v{api-version:apiVersion}/hello will have the parameter name api-version.

For additional information and end-to-end working examples, I recommend you review the API Documentation topic. There are links to the samples by platform.

@Ramya5108
Copy link

Hi @commonsensesoftware ,

Thanks for the reponse , Sorry that i missed v{version:apiVersion}/hello in the sample above , while actual code had this.

From the links you have provided , i understand that ordercontroller lied on 3 different namespaces for each version , to me i am looking for the same controller , same namespace , supporting multiple versions .

As well , the samples on the link used swashbuckle .aspnetcore package and asp.net core application.While i use swashbuckle and asp.net web application.

i agree that routetemplateparameter will be unaware of the value, Could you please help me with a link on the sample to tweak the swagger on this?is that possible?

https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/244 am facing the same issue , while not with asp.net core application , but using asp.net web application.Please help @commonsensesoftware !!

@commonsensesoftware
Copy link
Collaborator

@Ramya5108, the wiki links provided have the setup instructions and required NuGet packages for all ASP.NET platforms, including Web API. There are numerous, numerous combinations in which you might compose your application to support mapping code to services and API versions. You might use different namespaces, you might not. Ultimately, the API versioning libraries don't care which approach you use. They are able to discover your APIs and their versions. If you examine the V1 OrdersController, you'll see that it implements versions 0.9 and 1.0, which is the type of API version interleaving you are asking for. Both API versions will appear as separate entries in the Swagger document and UI. If you so desire, you put all of your services in a single namespace and apply API versions this way. The choice is yours.

Please review the complete end-to-end ASP.NET Web API with API Versioning and Swashbuckle sample.

I hope that helps.

@Ramya5108
Copy link

Thanks @commonsensesoftware for your inputs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants