I've researched this issue and found a lot of articles and also q+as on here but nothing for my scenario. I have an asp.net core 3 API with 2 versions, 1 and 2. The API has 3 consumers, ConA, ConB, and ConC, and 3 controllers. ConA accesses controllers 1 and 2, ConB accesses only controller 3, and ConC accesses one endpoint from controller 1 and one endpoint from controller 3. For v1 I show everything but I now have a requirement to filter v2 endpoints by API consumer.
What I'm trying to do is create a Swagger document for each consumer that only shows the endpoints they can access. It's easy to do for ConA and ConB as I can use [ApiExplorerSettings(GroupName = "v-xyz")]
where v-xyz can be restricted by consumer and then split the Swagger documents that way. The problem is showing the endpoints for ConC - they don't have their own controller so I can't give them a GroupName. Here's a simplified version of the code:
public void ConfigureServices(IServiceCollection services)
{
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VV";
options.SubstituteApiVersionInUrl = true;
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo() { Title = "My API - Version 1", Version = "v1.0" });
c.SwaggerDoc("v2-conA", new OpenApiInfo() { Title = "My API - Version 2", Version = "v2.0" });
c.SwaggerDoc("v2-conB", new OpenApiInfo() { Title = "My API - Version 2", Version = "v2.0" });
c.SwaggerDoc("v2-conC", new OpenApiInfo() { Title = "My API - Version 2", Version = "v2.0" });
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
c.EnableAnnotations();
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.EnableDeepLinking();
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.SwaggerEndpoint("/swagger/v2-conA/swagger.json", "My API V2 ConA");
c.SwaggerEndpoint("/swagger/v2-conB/swagger.json", "My API V2 ConB");
c.SwaggerEndpoint("/swagger/v2-conC/swagger.json", "My API V2 Con3");
});
}
Version 1 controllers:
[Route("api/account")]
[ApiController]
[ApiExplorerSettings(GroupName = "v1")]
public class AccountController : ControllerBase
{
[HttpGet("get-user-details")]
public ActionResult GetUserDetails([FromQuery]string userId)
{
return Ok(new { UserId = userId, Name = "John", Surname = "Smith", Version = "V1" });
}
}
[Route("api/account-admin")]
[ApiController]
[ApiExplorerSettings(GroupName = "v1")]
public class AccountAdminController : ControllerBase
{
[HttpPost("verify")]
public ActionResult Verify([FromBody]string userId)
{
return Ok($"{userId} V1");
}
}
[Route("api/notification")]
[ApiController]
[ApiExplorerSettings(GroupName = "v1")]
public class NotificationController : ControllerBase
{
[HttpPost("send-notification")]
public ActionResult SendNotification([FromBody]string userId)
{
return Ok($"{userId} V1");
}
}
Version 2 controllers (namespaced in separate folder "controllers/v2"):
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/account")]
[ApiController]
[ApiExplorerSettings(GroupName = "v2-conA")]
public class AccountController : ControllerBase
{
[HttpGet("get-user-details")]
[SwaggerOperation(Tags = new[] { "ConA - Account" })]
public ActionResult GetUserDetails([FromQuery]string userId)
{
return Ok($"{userId} V2");
}
}
[Route("api/v{version:apiVersion}/account-admin")]
[ApiController]
[ApiVersion("2.0")]
[ApiExplorerSettings(GroupName = "v2-conB")]
public class AccountAdminController : ControllerBase
{
[HttpPost("verify")]
[SwaggerOperation(Tags = new[] { "ConB - Account Admin", "ConC - Account Admin" })]
public ActionResult Verify([FromBody] string userId)
{
return Ok($"{userId} V2");
}
}
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/notification")]
[ApiController]
[ApiExplorerSettings(GroupName = "v2-conA")]
public class NotificationController : ControllerBase
{
[HttpPost("send-notification")]
[SwaggerOperation(Tags = new[] { "ConA - Notification", "ConC - Notification" })]
public ActionResult SendNotification([FromBody] string userId)
{
return Ok($"{userId} V2");
}
}
This gets me some of the way in that I can see the endpoints for ConA and ConB, although it's not perfect as it's showing duplicate endpoints, but I'm stuck on how to show the endpoints for ConC (who can see one endpoint from controller 1 and one from controller 3). My next attempt will be to go back to showing all endpoints in version 2 and then filtering using IDocumentFilter if I can't get the above working somehow. Any thoughts or tips greatly appreciated 👍
I had to do this recently, we also had multiple consumers and needed to filter the endpoints per consumer. I used a DocumentFilter and filtered the endpoints using tags.
There's a fair bit of code in it so I stuck the full solution on Github: https://github.com/cbruen1/SwaggerFilter
public class Startup
{
private static Startup Instance { get; set; }
private static string AssemblyName { get; }
private static string FullVersionNo { get; }
private static string MajorMinorVersionNo { get; }
static Startup()
{
var fmt = CultureInfo.InvariantCulture;
var assemblyName = Assembly.GetExecutingAssembly().GetName();
AssemblyName = assemblyName.Name;
FullVersionNo = string.Format(fmt, "v{0}", assemblyName.Version.ToString());
MajorMinorVersionNo = string.Format(fmt, "v{0}.{1}",
assemblyName.Version.Major, assemblyName.Version.Minor);
}
public Startup(IConfiguration configuration)
{
Configuration = configuration;
Instance = this;
}
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();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VV";
options.SubstituteApiVersionInUrl = true;
});
// Use an IConfigureOptions for the settings
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen(c =>
{
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
// Group by tag
c.EnableAnnotations();
// Include comments for current assembly - right click the project and turn on this otion in the build properties
var xmlFile = $"{AssemblyName}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.EnableDeepLinking();
// Build a swagger endpoint for each API version and consumer
c.SwaggerEndpoint($"/swagger/{Constants.ApiVersion1}/swagger.json", "MyAccount API V1");
c.SwaggerEndpoint($"/swagger/{Constants.ApiConsumerGroupNameConA}/swagger.json", $"MyAccount API V2 {Constants.ApiConsumerNameConA}");
c.SwaggerEndpoint($"/swagger/{Constants.ApiConsumerGroupNameConB}/swagger.json", $"MyAccount API V2 {Constants.ApiConsumerNameConB}");
c.SwaggerEndpoint($"/swagger/{Constants.ApiConsumerGroupNameConC}/swagger.json", $"MyAccount API V2 {Constants.ApiConsumerNameConC}");
c.DocExpansion(DocExpansion.List);
});
}
}
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
public void Configure(SwaggerGenOptions options)
{
// Filter out api-version parameters globally
options.OperationFilter<ApiVersionFilter>();
// Create Swagger documents per version and consumer
options.SwaggerDoc(Constants.ApiVersion1, CreateInfoForApiVersion("v1.0", "My Account API V1"));
options.SwaggerDoc(Constants.ApiConsumerGroupNameConA, CreateInfoForApiVersion("v2.0", $"My Account API V2 {Constants.ApiConsumerNameConA}"));
options.SwaggerDoc(Constants.ApiConsumerGroupNameConB, CreateInfoForApiVersion("v2.0", $"My Account API V2 {Constants.ApiConsumerNameConB}"));
options.SwaggerDoc(Constants.ApiConsumerGroupNameConC, CreateInfoForApiVersion("v2.0", $"My Account API V2 {Constants.ApiConsumerNameConC}"));
// Include all paths
options.DocInclusionPredicate((name, api) => true);
// Filter endpoints based on consumer
options.DocumentFilter<SwaggerDocumentFilter>();
// Take first description on any conflict
options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
}
static OpenApiInfo CreateInfoForApiVersion(string version, string title)
{
var info = new OpenApiInfo()
{
Title = title,
Version = version
};
return info;
}
}
public class SwaggerDocumentFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
// Key is read-only so make a copy of the Paths property
var pathsPerConsumer = new OpenApiPaths();
var currentConsumer = GetConsumer(swaggerDoc.Info.Title);
IDictionary<string, OpenApiSchema> allSchemas = swaggerDoc.Components.Schemas;
if (swaggerDoc.Info.Version.Contains(Constants.ApiVersion2))
{
foreach (var path in swaggerDoc.Paths)
{
// If there are any tags (all methods are decorated with "SwaggerOperation(Tags = new[]...") with the current consumer name
if (path.Value.Operations.Values.FirstOrDefault().Tags
.Where(t => t.Name.Contains(currentConsumer)).Any())
{
// Remove tags not applicable to the current consumer (for endpoints where multiple consumers have access)
var newPath = RemoveTags(currentConsumer, path);
// Add the path to the collection of paths for current consumer
pathsPerConsumer.Add(newPath.Key, newPath.Value);
}
}
//// Whatever objects are used as parameters or return objects in the API will be listed under the Schemas section in the Swagger UI
//// Use below to filter them based on the current consumer - remove schemas not belonging to the current path
//foreach (KeyValuePair<string, OpenApiSchema> schema in allSchemas)
//{
// // Get the schemas for current consumer
// if (Constants.ApiPathSchemas.TryGetValue(currentConsumer, out List<string> schemaList))
// {
// if (!schemaList.Contains(schema.Key))
// {
// swaggerDoc.Components.Schemas.Remove(schema.Key);
// }
// }
//}
}
else
{
// For version 1 list version 1 endpoints only
foreach (var path in swaggerDoc.Paths)
{
if (!path.Key.Contains(Constants.ApiVersion2))
{
pathsPerConsumer.Add(path.Key, path.Value);
}
}
}
swaggerDoc.Paths = pathsPerConsumer;
}
public KeyValuePair<string, OpenApiPathItem> RemoveTags(string currentConsumer, KeyValuePair<string, OpenApiPathItem> path)
{
foreach (var item in path.Value.Operations.Values?.FirstOrDefault().Tags?.ToList())
{
// If the tag name doesn't contain the current consumer name remove it
if (!item.Name.Contains(currentConsumer))
{
path.Value.Operations.Values?.FirstOrDefault().Tags?.Remove(item);
}
}
return path;
}
private string GetConsumer(string path)
{
if (path.Contains(Constants.ApiConsumerNameConA))
{
return Constants.ApiConsumerNameConA;
}
else if (path.Contains(Constants.ApiConsumerNameConB))
{
return Constants.ApiConsumerNameConB;
}
else if (path.Contains(Constants.ApiConsumerNameConC))
{
return Constants.ApiConsumerNameConC;
}
return string.Empty;
}
}
public class ApiVersionFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Remove version parameter field from Swagger UI
var parametersToRemove = operation.Parameters.Where(x => x.Name == "api-version").ToList();
foreach (var parameter in parametersToRemove)
{
operation.Parameters.Remove(parameter);
}
}
}
public static class Constants
{
// Swagger UI grouping and filtering
public const string ApiVersion1 = "v1";
public const string ApiVersion2 = "v2";
// The full consumer name
public const string ApiConsumerNameConA = "Consumer A";
public const string ApiConsumerNameConB = "Consumer B";
public const string ApiConsumerNameConC = "Consumer C";
// Specify the group name - this appears in the Swagger UI drop-down
public const string ApiConsumerGroupNameConA = "v2-conA";
public const string ApiConsumerGroupNameConB = "v2-conB";
public const string ApiConsumerGroupNameConC = "v2-conC";
// Decorate each controller method with the tag names below - this determines
// what consumer can access what endpoint, and also how the endpoints are
// grouped and named in the Swagger UI
// Swagger ConA tag names
public const string ApiConsumerTagNameConAAccount = ApiConsumerNameConA + " - Account";
public const string ApiConsumerTagNameConANotification = ApiConsumerNameConA + " - Notification";
// Swagger ConB tag names
public const string ApiConsumerTagNameConBAccountAdmin = ApiConsumerNameConB + " - Account Admin";
// Swagger ConC tag names
public const string ApiConsumerTagNameConCAccountAdmin = ApiConsumerNameConC + " - Account Admin";
public const string ApiConsumerTagNameConCNotification = ApiConsumerNameConC + " - Notification";
// Store the schemes belonging to each Path for Swagger so only the relevant ones are shown in the Swagger UI
public static IReadOnlyDictionary<string, List<string>> ApiPathSchemas;
static Constants()
{
ApiPathSchemas = new Dictionary<string, List<string>>()
{
//// Whatever objects are used as parameters or return objects in the API will be listed under the Schemas section in the Swagger UI
//// Use below to add the list required by each consumer
// Consumer A has access to all so only specify those for B and C
// { ApiConsumerNameConB, new List<string>() { "SearchOutcome", "AccountDetails", "ProblemDetails" }},
// { ApiConsumerNameConC, new List<string>() { "NotificationType", "SendNotificationRequest", "ProblemDetails" }}
};
}
}
// v1 controllers
[Route("api/account-admin")]
[ApiController]
[ApiExplorerSettings(GroupName = Constants.ApiVersion1)]
public class AccountAdminController : ControllerBase
{
[HttpPost("verify")]
public ActionResult Verify([FromBody]string userId)
{
return Ok($"{userId} V1");
}
}
[ApiController]
[ApiExplorerSettings(GroupName = Constants.ApiVersion1)]
public class AccountController : ControllerBase
{
[HttpGet("api/account/get-user-details")]
public ActionResult GetUserDetails([FromQuery]string userId)
{
return Ok(new { UserId = userId, Name = "John", Surname = "Smith", Version = "V1" });
}
}
[Route("api/notification")]
[ApiController]
[ApiExplorerSettings(GroupName = Constants.ApiVersion1)]
public class NotificationController : ControllerBase
{
[HttpPost("send-notification")]
public ActionResult SendNotification([FromBody]string userId)
{
return Ok($"{userId} V1");
}
}
// v2 controllers
[Route("api/v{version:apiVersion}/account-admin")]
[ApiController]
[ApiVersion("2.0")]
public class AccountAdminController : ControllerBase
{
[HttpPost("verify")]
[SwaggerOperation(Tags = new[] { Constants.ApiConsumerTagNameConBAccountAdmin, Constants.ApiConsumerTagNameConCAccountAdmin })]
public ActionResult Verify([FromBody] string userId)
{
return Ok($"{userId} V2");
}
}
[Route("api/v{version:apiVersion}/account")]
[ApiController]
[ApiVersion("2.0")]
public class AccountController : ControllerBase
{
[HttpGet("get-user-details")]
[SwaggerOperation(Tags = new[] { Constants.ApiConsumerTagNameConAAccount })]
public ActionResult GetUserDetails([FromQuery]string userId)
{
return Ok($"{userId} V2");
}
}
[Route("api/v{version:apiVersion}/notification")]
[ApiController]
[ApiVersion("2.0")]
public class NotificationController : ControllerBase
{
[HttpPost("send-notification")]
[SwaggerOperation(Tags = new[] { Constants.ApiConsumerTagNameConANotification, Constants.ApiConsumerTagNameConCNotification })]
public ActionResult SendNotification([FromBody] string userId)
{
return Ok($"{userId} V2");
}
}
Solution structure:
API filtered for Consumer C:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With