简体   繁体   中英

Core 3.1 C# multiple API versions Swagger UI not separating endpoints properly

I'm having an issue slightly different than the other questions pertaining to documentation of multiple APIs in the Swagger UI.

I am getting two .json documents generated as expected, and the UI has the dropdown in the upper-right allowing me to switch between the v1 documentation and the v2 documentation.

This is my issue: The v1 document displays the one available action for the v1 API. However, the v2 document displays the available actions for both v1 and v2.

Here's the full code from my startup.cs file:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using netCoreV3_1ApiStarter.Entities;
using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Versioning;

namespace netCoreV3_1ApiStarter
{
  public class Startup
  {
    public Startup(IConfiguration configuration)
    {
      Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
      #region API Versioning
      services.AddApiVersioning(c =>
      {
        c.DefaultApiVersion = new ApiVersion(1, 0);
        c.AssumeDefaultVersionWhenUnspecified = true;
        c.ReportApiVersions = true;
        c.ApiVersionReader = new UrlSegmentApiVersionReader();
      });
      #endregion

      services.AddControllers();

      services.AddSwaggerGen(c =>
      {
        c.SwaggerDoc("v1", new OpenApiInfo
        {
          Title = $"{Configuration.GetSection("ApplicationName").Value} API",
          Version = "v1",
          Description = $"API v1 methods available for the {Configuration.GetSection("ApplicationName").Value} system",
          Contact = new OpenApiContact
          {
            Email = "blah@blah.com",
            Name = "Nunya",
            Url = new System.Uri("http://www.apple.com")
          }
        });

        c.SwaggerDoc("v2", new OpenApiInfo
        {
          Title = $"{Configuration.GetSection("ApplicationName").Value} API",
          Version = "v1",
          Description = $"API v2 methods available for the {Configuration.GetSection("ApplicationName").Value} system",
          Contact = new OpenApiContact
          {
            Email = "blah@blah.com",
            Name = "Nunya",
            Url = new System.Uri("http://www.apple.com")
          }
        });

        c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
        c.DocInclusionPredicate((docName, apiDesc) => apiDesc.GroupName == docName);
      });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
      if (env.IsDevelopment())
      {
        app.UseDeveloperExceptionPage();
      }

      app.UseSwagger();

      app.UseSwaggerUI(c =>
      {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", $"{Configuration.GetSection("ApplicationName").Value} API v1");
        c.SwaggerEndpoint("/swagger/v2/swagger.json", $"{Configuration.GetSection("ApplicationName").Value} API v2");
        c.RoutePrefix = string.Empty;
      });

      app.UseRouting();
      app.UseAuthorization();
      app.UseEndpoints(endpoints =>
      {
        endpoints.MapControllers();
      });
    }
  }
}

I have my Controllers separated into sub-folders by API versions in an attempt to both organize and prevent namespace collisions, etc.

The v1 WeatherForecastController.cs file:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

namespace netCoreV3_1ApiStarter.Controllers.v1
{
  [ApiVersion("1.0")]
  [ApiExplorerSettings(GroupName = "v1")]
  [Route("api/v{version:apiVersion}/[controller]")]
  [ApiController]
  public class WeatherForecastController : ControllerBase
  {
    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
      _logger = logger;
    }

    private static readonly string[] Summaries = new[]
    {
      "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
      var rng = new Random();
      return Enumerable.Range(1, 5).Select(index => new WeatherForecast
      {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
      })
      .ToArray();
    }
  }
}

And, my v2 WeatherForecastController.cs :

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace netCoreV3_1ApiStarter.Controllers.v2
{
  [ApiVersion("2.0")]
  [ApiExplorerSettings(GroupName = "v2")]
  [Route("api/v{version:apiVersion}/[controller]")]
  [ApiController]
  public class WeatherForecastController : ControllerBase
  {
    // GET: api/<WeatherForecastController>
    [HttpGet]
    public IEnumerable<string> Get()
    {
      return new string[] { "value1", "value2" };
    }

    // GET api/<WeatherForecastController>/5
    [HttpGet("{id}")]
    public string Get(int id)
    {
      return $"API v2 {id}";
    }

    // POST api/<WeatherForecastController>
    [HttpPost]
    public void Post([FromBody] string value)
    {
    }

    // PUT api/<WeatherForecastController>/5
    [HttpPut("{id}")]
    public void Put(int id, [FromBody] string value)
    {
    }

    // DELETE api/<WeatherForecastController>/5
    [HttpDelete("{id}")]
    public void Delete(int id)
    {
    }
  }
}

Any suggestions as to what I'm missing to get the v1 screen to show the available v1 action and the v2 screen to show the available v2 actions?

According to your request, is it only showing some APIs of v2 when switching the V2 version?

You can do this:

Versions Used

ASP.NET Core 3.1
Swashbuckle.AspNetCore: 5.4.1

First I created 2 versions of folders and controllers:

在此处输入图片说明

Therefore, the namespace of each controller corresponds to its folder, like this:

V1 version

namespace WebApplication129.Controllers.V1
{
    [ApiController]
    [Route("api/v1/[controller]")]
    public class HomeController : ControllerBase
    {

       [Route("test")]
        [HttpGet]
        public string Test()
        {
            return "v1 test";
        }
    }
}

V2 version

namespace WebApplication129.Controllers.V2
{
    [ApiController]
    [Route("api/v2/[controller]")]
    public class HomeController : ControllerBase
    {

        [Route("test")]
        [HttpGet]
        public string Test()
        {
            return "v2 test";
        }
    }
}

Then create an agreement to inform Swagger, in this way, we can control how Swagger generates Swagger documents, thereby controlling the UI.

Create the following class:

public class GroupingByNamespaceConvention : IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        var controllerNamespace = controller.ControllerType.Namespace;
        var apiVersion = controllerNamespace.Split(".").Last().ToLower();
        if (!apiVersion.StartsWith("v")) { apiVersion = "v1"; }
        controller.ApiExplorer.GroupName = apiVersion;
    }
}

What we do is group controllers according to the last segment of their namespace. Thus, the controllers whose namespace ends in v1 will be grouped under the name “v1“, those that end in v2 are grouped as “v2“, etc.

Now we must apply the convention. For that we go to AddControllers in ConfigureServices and add the convention:

services.AddControllers(options =>
{
    options.Conventions.Add(new GroupingByNamespaceConvention());
});

The final complete startup.cs configuration is as follows:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using System;
using WebApplication129.Controllers.conf;

namespace WebApplication129
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers(options =>
            {
                options.Conventions.Add(new GroupingByNamespaceConvention());
            });

            services.AddSwaggerGen(config =>
            {
                var titleBase = "Test API";
                var description = "This is a Web API for Test operations";
                var TermsOfService = new Uri("https://xxxxxx");
                var License = new OpenApiLicense()
                {
                    Name = "Test"
                };
                var Contact = new OpenApiContact()
                {
                    Name = "Test",
                    Email = "Test@hotmail.com",
                    Url = new Uri("https://xxxxxx")
                };

                config.SwaggerDoc("v1", new OpenApiInfo
                {
                    Version = "v1",
                    Title = titleBase + " v1",
                    Description = description,
                    TermsOfService = TermsOfService,
                    License = License,
                    Contact = Contact
                });

                config.SwaggerDoc("v2", new OpenApiInfo
                {
                    Version = "v2",
                    Title = titleBase + " v2",
                    Description = description,
                    TermsOfService = TermsOfService,
                    License = License,
                    Contact = Contact
                });
            });
        }

       
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseSwagger();
            app.UseSwaggerUI(config =>
            {
                config.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
                config.SwaggerEndpoint("/swagger/v2/swagger.json", "v2");
            });
            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Visit url: https://localhost:yourport/swagger/index.html

Result:

在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM