如何解决使用 Swagger UI 和 Swashbuckle 按消费者过滤 API 端点
我研究了这个问题,发现了很多文章和 q+as 在这里,但没有适合我的场景。我有一个 asp.net core 3 API,有 2 个版本,1 和 2。API 有 3 个使用者,ConA、ConB 和 ConC,以及 3 个控制器。 ConA 访问控制器 1 和 2,ConB 仅访问控制器 3,ConC 从控制器 1 访问一个端点,从控制器 3 访问一个端点。对于 v1,我展示了所有内容,但我现在需要按 API 使用者过滤 v2 端点。
我想做的是为每个消费者创建一个 Swagger 文档,只显示他们可以访问的端点。 ConA 和 ConB 很容易做到,因为我可以使用 [ApiExplorerSettings(GroupName = "v-xyz")]
,其中 v-xyz 可以被消费者限制,然后以这种方式拆分 Swagger 文档。问题是显示 ConC 的端点 - 他们没有自己的控制器,所以我不能给他们一个 GroupName。这是代码的简化版本:
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",Version = "v2.0" });
c.SwaggerDoc("v2-conC",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");
});
}
版本 1 控制器:
[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");
}
}
第 2 版控制器(命名空间位于单独的文件夹“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");
}
}
这让我有了一些方法,我可以看到 ConA 和 ConB 的端点,虽然它并不完美,因为它显示了重复的端点,但我一直在如何显示 ConC 的端点(谁可以看到一个来自控制器 1 的端点和来自控制器 3 的端点)。我的下一次尝试将返回显示版本 2 中的所有端点,然后如果我无法以某种方式使上述工作正常工作,则使用 IDocumentFilter 进行过滤。非常感谢任何想法或提示?
解决方法
我最近不得不这样做,我们还有多个消费者,需要过滤每个消费者的端点。我使用了 DocumentFilter 并使用标签过滤了端点。
里面有很多代码,所以我把完整的解决方案放在 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,$"My Account API V2 {Constants.ApiConsumerNameConB}"));
options.SwaggerDoc(Constants.ApiConsumerGroupNameConC,$"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");
}
}
解决方案结构:
针对消费者 C 过滤的 API:
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。