【问题标题】:Filter API endpoints by consumer using Swagger UI and Swashbuckle使用 Swagger UI 和 Swashbuckle 按消费者过滤 API 端点
【发布时间】:2020-12-25 16:10:05
【问题描述】:

我研究了这个问题,发现了很多文章和 q+as on here,但对我的场景没有任何帮助。我有一个 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", 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");
    });
}

版本 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 进行过滤。任何想法或提示非常感谢????

【问题讨论】:

    标签: swagger asp.net-core-webapi swagger-ui swashbuckle.aspnetcore


    【解决方案1】:

    我最近不得不这样做,我们还有多个消费者,需要过滤每个消费者的端点。我使用了 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, 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");
        }
    }
    

    解决方案结构:

    为消费者 C 过滤的 API:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-07-29
      • 2022-12-21
      • 2023-03-12
      • 1970-01-01
      • 2016-08-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多