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 = "[email protected]",
Name = "Nunya",
Url = new System.Uri("http://www.apple.com")
}
});
c.SwaggerDoc("v2", new OpenApiInfo
{
Title = $"{Configuration.GetSection("ApplicationName").Value} API",
Version = "v2",
Description = $"API v2 methods available for the {Configuration.GetSection("ApplicationName").Value} system",
Contact = new OpenApiContact
{
Email = "[email protected]",
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?
Edit 10/21/2021: Fixed the typo in the second SwaggerDoc object definition. Incorrectly referenced v1 when it should've referenced v2. No difference in the performance, however.
CodePudding user response:
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 = "[email protected]",
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: