Фильтрация конечных точек API по потребителю с помощью пользовательского интерфейса Swagger и Swashbuckle

Я исследовал эту проблему и нашел много статей, а также q +, как здесь, но ничего для моего сценария. У меня есть API asp.net core 3 с 2 версиями, 1 и 2. API имеет 3 потребителя, ConA, ConB и ConC, а также 3 контроллера. ConA обращается к контроллерам 1 и 2, ConB обращается только к контроллеру 3, а ConC получает доступ к одной конечной точке от контроллера 1 и одной конечной точке от контроллера 3. Для v1 я показываю все, но теперь у меня есть требование фильтровать конечные точки v2 по потребителю API.

Я пытаюсь создать документ 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 (в отдельной папке контроллеры / 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

Конечные точки Swagger с использованием приведенного выше кода

Нет конечных точек для ConC :(


person some_randomer    schedule 25.12.2020    source источник


Ответы (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");
    }
}

Структура решения:

Структура решения

API отфильтрован для потребителя C:

API отфильтрован для потребителя C

person Ciarán Bruen    schedule 20.03.2021