0

Swagger with .Net 5 REST API.

I have an API with hundreds of methods that necessitates the use of grouping related APIs together. At the same time, I need to start work on a V2 version of my API. I have spent hours trying different approaches to implement versioning AND grouping without success.

I have several controllers, some for V1 and some for V2. Can someone offer a complete example?

abdusco
  • 9,700
  • 2
  • 27
  • 44
Matthew
  • 171
  • 2
  • 10
  • 2
    _"trying different approaches"_ what in particular? _"several controllers, some for V1 and some for V2"_, post how you versioned the endpoints – abdusco Aug 15 '21 at 08:16
  • https://github.com/dotnet/aspnet-api-versioning/issues/516, https://www.youtube.com/watch?v=rQqsII9iyPk, https://morioh.com/p/80c5e06b62de, https://rimdev.io/swagger-grouping-with-controller-name-fallback-using-swashbuckle-aspnetcore/ All suffered the same problem. I could get versioning and grouping to work with V1, but with V2, the grouping didn't work. – Matthew Aug 15 '21 at 16:18
  • Have you looked into writing a schema filter or an operation filter? – abdusco Aug 15 '21 at 17:41
  • I tried using grouping, and based on the name of the controllers. Is there an example somewhere that I can base a solution on? Is this not an obvious need? I would think this is something that would be supported out-of-the-box without a huge amount of customization? – Matthew Aug 16 '21 at 20:58
  • https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/README.md#customize-operation-tags-eg-for-ui-grouping – abdusco Aug 16 '21 at 21:38
  • abdusco, I don't see how this helps. – Matthew Aug 17 '21 at 19:27
  • When combined with [API Versioning](https://github.com/dotnet/aspnet-api-versioning), the API Version _is_ the grouping. The design of the API Explorer intrinsically only supports _group name_, which means there is only a single dimension of grouping. While it's possible to create multi-dimensional grouping (e.g. name + version), it likely involves more customization than you're interesting in putting in. If you can share what you want the structure to be like, I can provide a complete answer. – Chris Martinez Aug 17 '21 at 19:32
  • The solution suggested by abdusco says it is "obsolete". My needs are simple. I have several hundred APIs. I need to group related APIs in logical ways. Currently, I'm using this code: [ApiExplorerSettings(GroupName = "Utilities")] But this no longer works when I implemented versioning. Actually V1 works, but V2 does not. Apparently, this is well known since I have seen may similar posts that describe my issue (if you apply grouping, no APIs are listed) Your assistance is greatly appreciated. – Matthew Aug 19 '21 at 12:15

2 Answers2

1

Before jumping into a solution, let me qualify a few points:

  • The YouTube video above and @abdusco are providing their own approach to API Versioning in their opinion, but the question is about ASP.NET Core API Versioning
  • Although not specified, I'm going to assume you are not versioning by URL segment
  • The default OpenAPI/Swagger UI only has a single pivot point (e.g. group) which you can select in a dropdown
  • An OpenAPI/Swagger document must have unique URLs, which means you cannot have multiple APIs in the same document unless you version by URL segment (worst choice; least RESTful)
  • By Swagger, I presume you mean Swashbuckle. The Swashbuckle OpenAPI generator uses ApiDescription.GroupName to collate APIs in a single document per group, which maps to the dropdown in UI. Changing this behavior is possible, but non-trivial.
  • The default behavior of the API Explorer extensions for API Versioning sets the ApiDescription.GroupName to a formatted API version value if not otherwise set so that APIs are grouped by version. Prior to 4.1, [ApiExplorerSettings(GroupName="...")] was ignored and overwritten.

At the end of the day, you are asking for two grouping dimensions: category and API version. Unfortunately, the API Explorer, OpenAPI/Swagger UI, and Swashbuckle only intrinsically support one. Any form of hierarchy is not supported out-of-the-box. There a couple of ways you can get around this:

  1. Concatenate group name and API version as a single dimension
  2. Create a Swashbuckle extension (ex: IOperationFilter) to add OpenAPI tags for a particular group. The UI can then be changed to add a second dropdown based on those tags for filtering or the tags can be used for collated groups (think Expander controls).
  3. Version by URL segment, produce a single OpenAPI document, update the GroupName back the way you want (This approach is not recommended)

There might be more options, but these are the most sensible. While it might not be what you're hoping for, given the limitations to work with, #3 is the most straight forward to implement. It would look like this:

public class SubgroupDescriptionProvider : IApiDescriptionProvider
{
    private readonly IOptions<ApiExplorerOptions> options;

    public SubgroupDescriptionProvider(IOptions<ApiExplorerOptions> options)
        => this.options = options;

    // Execute after DefaultApiVersionDescriptionProvider.OnProvidersExecuted
    public int Order => -1;

    public void OnProvidersExecuting(ApiDescriptionProviderContext context) { }

    public void OnProvidersExecuted(ApiDescriptionProviderContext context)
    {
        var format = options.Value.GroupNameFormat;
        var culture = CultureInfo.CurrentCulture;
        var results = context.Results;
        var newResults = new List<ApiDescription>(capacity: results.Count);

        for (var i = 0; i < results.Count; i++)
        {
            var result = results[i];
            var apiVersion = result.GetApiVersion();
            var versionGroupName = apiVersion.ToString(format, culture);

            // [ApiExplorerSettings(GroupName="...")] was NOT set so
            // nothing else to do
            if (result.GroupName == versionGroupName)
            {
                continue;
            }

            // must be using [ApiExplorerSettings(GroupName="...")] so
            // concatenate it with the formatted API version
            result.GroupName += " " + versionGroupName;

            // optional: add version grouping as well
            // note: this works because the api description will appear in
            // multiple, but different, documents
            var newResult = result.Clone();

            newResult.GroupName = versionGroupName;
            newResults.Add(newResult);
        }

        newResults.ForEach(results.Add);
    }
}

You then register this with DI using:

services.TryAddEnumerable(
    ServiceDescriptor.Transient<IApiDescriptionProvider, SubgroupDescriptionProvider>());

If you use both sets of grouping, then you'll get multiple OpenAPI documents.

By API Version (default)

├─ v1
│   ├─ /utility
│   └─ /not-utility
└─ v2
    ├─ /utility
    └─ /not-utility

By Group Name + API Version

├─ Utilities v1   
│   └─ /utility
├─ Not Utilities v1
│   └─ /not-utility
├─ Utilities v2
│   └─ /utility
└─ Not Utilities v2
    └─ /not-utility

If your versioning policy is something like N-2, then the list should be reasonable. It's a product of group * version, so the list will depend on how many groups and versions you ultimately have.

Having a true, two dimensional hierarchy should be technically feasible, but I've never done it nor known anyone willing to put the effort in to make it happen. The solution would be pretty specific your set of APIs.

Chris Martinez
  • 3,185
  • 12
  • 28
  • Gratitude is an important personal trait that I try to foster in myself, but frankly with my limited knowledge of Swagger/Swashbuckle, I simply don't know what to do with your example. I don't know how I'm supposed to implement versioning, nor do I know how I need to annotate my controllers and API to work with your suggestion. There simply is insufficient information for me to build this from end-to-end. Should I use this example for versioning? https://jones.bz/web-api-versioning-with-swagger/ – Matthew Aug 23 '21 at 19:47
  • Ok. I see. I got the impression that you were already familiar with the concepts and just wanted to customize things. We should step back. I recommend starting with the [Swagger Example](https://github.com/dotnet/aspnet-api-versioning/blob/master/samples/aspnetcore/SwaggerSample) that combines API Versioning with Swashbuckle. It's a comprehensive soup-to-nuts example you can run and play with. _How_ you want to version is highly subjective. If you care about being RESTful then you should choose the media type negotiation or query string method. – Chris Martinez Aug 23 '21 at 21:26
  • I've done my best to put a lot into the [wiki](https://github.com/dotnet/aspnet-api-versioning/wiki). There is a ton of information in there covering many options and variations. There are also many different posts, videos, and even training sessions available online now, but I don't endorse one over another. I'm happy to answer clarifying questions. It might even be preferable to get comfortable with how versioning works before slapping on OpenAPI/Swagger/Swashbuckle. – Chris Martinez Aug 23 '21 at 21:29
0

Use the below code in program.cs (.net 6), If you are using .net 5(or below version) use this code in strartup.cs

        services.AddVersionedApiExplorer(options =>
          {
            options.GroupNameFormat = "'v'VV";
            options.SubstituteApiVersionInUrl = true;
          });

         services.AddSwaggerGen(options =>
          {

            options.DocInclusionPredicate((documentName, apiDescription) =>
            {
                var actionApiVersionModel = apiDescription.ActionDescriptor.GetApiVersionModel();
                var apiExplorerSettingsAttribute = (ApiExplorerSettingsAttribute)apiDescription.ActionDescriptor.EndpointMetadata.First(x => x.GetType().Equals(typeof(ApiExplorerSettingsAttribute)));
                if (actionApiVersionModel == null)
                {
                    return true;
                }
                if (actionApiVersionModel.DeclaredApiVersions.Any())
                {
                    return actionApiVersionModel.DeclaredApiVersions.Any(v =>
                    $"{apiExplorerSettingsAttribute.GroupName}v{v}" == documentName);
                }
                return actionApiVersionModel.ImplementedApiVersions.Any(v =>
                       $"{apiExplorerSettingsAttribute.GroupName}v{v}" == documentName);
            });

            var apiVersionDescriptionProvider = services.BuildServiceProvider().GetService<IApiVersionDescriptionProvider>();
            foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
            {
                options.SwaggerDoc($"Orders{description.GroupName}", new OpenApiInfo
                {
                    Title = "Example Service",
                    Description = "Example Service -- Backend Service Project",
                    Version = description.ApiVersion.ToString(),
                    
                });
                options.SwaggerDoc($"Payments{description.GroupName}", new OpenApiInfo
                {
                    Title = "Example Service",
                    Description = "Example Service -- Backend Service Project",
                    Version = description.ApiVersion.ToString(),
                });
                // Orders & Payments are the groupNames, above ex: out put will be : swagger/OrdersV1.0/swagger.json ( if you select Orders from the swagger definition dropdown) 
                // If you select Payments => output : swagger/PaymentsV1.0/swagger.json
            }
            
            
//var app = builder.Build();
                
            app.UseSwagger();

            app.UseSwaggerUI(
            swaggerOptions =>
            {                   
                foreach (var description in provider.ApiVersionDescriptions)
                {
                    swaggerOptions.SwaggerEndpoint($"{swagger/Orders{description.GroupName}/swagger.json",$"Orders  {description.GroupName.ToUpperInvariant()}" );
                    swaggerOptions.SwaggerEndpoint($"{swagger/Payments{description.GroupName}/swagger.json", $"Payments  {description.GroupName.ToUpperInvariant()}");
                }
            }); 
            
            
            

Controller //V1 // example controller 1:

[ApiVersion("1.0")]
[ApiController]
[ApiExplorerSettings(GroupName = "Orders")]
[Route("api/v{version:apiVersion}/[controller]")]
public class OrdersController : ControllerBase              
            

// example controller 2:

[ApiVersion("1.0")]
[ApiController]
[ApiExplorerSettings(GroupName = "Payments")]
[Route("api/v{version:apiVersion}/[controller]")]
public class PaymentsController : ControllerBase    
Shareef
  • 11
  • 1