Swashbuckle.aspnetcore: Dynamically Changing the Swagger UI Endpoints at Runtime

Created on 26 Mar 2019  路  5Comments  路  Source: domaindrivendev/Swashbuckle.AspNetCore

Is there a way of re-populating the endpoints Swagger-UI uses at runtime?

Currently, when building the Swagger UI endpoints we would do something like this:

   // This is an example of what we currently do.
   app.UseSwaggerUI(setup => {
      // Get running services from k8s or config (when running locally).
      var services = serviceDiscovery.GetServices();
      foreach(var server in services) {
          app.SwaggerEndpoint(server.Name, $"http://{server.LocalDns}/swagger/swagger.json");
      }
   });

We'd like to be able to programmatically refresh that list based on service discovery inside of our k8s cluster.

As new services come up, old ones removed and existing ones updated we'd currently have to bounce the docs pod/ service that generates that list.

By refreshing it - on a timer, or via a web-hook from our build server - the changes would be automatically reflected.

p3

Most helpful comment

In the meanwhile I've been trying a few things. Bear in mind I'm not very familiar with the .net core Options pattern, so forgive me if what follows next seems silly.

Without changing anything in the swashbuckle code, I tried injecting both IOptions<SwaggerGenOptions> swaggerGenOptions and IOptions<SwaggerUIOptions> swaggerUiOptions in the service where new modules are added.
I then go on to call swaggerGenOptions.SwaggerDoc(...); and swaggerUiOptions.SwaggerEndpoint(...);

Subsequent calls to this service seem to reflect that the additions have been made, however upon reloading the swagger UI in the browser, or trying to acces the .json directly, the changes are not reflected.

What am I missing here?

I've been reading up a bit on IOptionsMonitor and IOptionsSnapshot, but I'm not entirely sure these are of use here.

Any thoughts?

All 5 comments

This is a reasonable request but unfortunately it's a bit far back on the list of priorities. With that said, the Swashbuckle components (including the UI middleware) is already hooked into the ASP.NET Core configuration model and so adding support for this may be a fairly trivial endeavor if someone wanted to take it on and submit a PR. The following article might be of use in doing so - https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-2.2#reload-configuration-data-with-ioptionssnapshot

@domaindrivendev Thanks! If I get some space, I'll take a look.

In the mean time, I've got a brute force approach of an authorized endpoint that kills the service! k8s then re-creates it for me. Not ideal, but it works!

Cheers!

Are there any updates on this?

In our use case we a have a plugin architecture in which Controllers are loaded ad hoc using ApplicationPartManager. As long as we use a single swagger doc, it gets updated nicely. However, we would prefer to have a swagger doc per module.
Each module has a seperate Area, which is what we use to filter in DocInclusionPredicate.

Any thoughts on how to implement the at runtime addition of swagger endpoints?

In the meanwhile I've been trying a few things. Bear in mind I'm not very familiar with the .net core Options pattern, so forgive me if what follows next seems silly.

Without changing anything in the swashbuckle code, I tried injecting both IOptions<SwaggerGenOptions> swaggerGenOptions and IOptions<SwaggerUIOptions> swaggerUiOptions in the service where new modules are added.
I then go on to call swaggerGenOptions.SwaggerDoc(...); and swaggerUiOptions.SwaggerEndpoint(...);

Subsequent calls to this service seem to reflect that the additions have been made, however upon reloading the swagger UI in the browser, or trying to acces the .json directly, the changes are not reflected.

What am I missing here?

I've been reading up a bit on IOptionsMonitor and IOptionsSnapshot, but I'm not entirely sure these are of use here.

Any thoughts?

Hello all,
Working with SwaggerForOcelot and the ability to auto-discover the endpoints I created a not so elegant solution that can help someone :)

Create a wrapper over the current SwaggerUIMiddleware to work with dynamic swagger endpoints

    /// <summary>
    /// Wrapper over SwaggerUI middleware to support reloading the options at runtime
    /// </summary>
    public class KubeSwaggerUiMiddleware : IMiddleware
    {
        private readonly IWebHostEnvironment hostingEnv;
        private readonly ILoggerFactory loggerFactory;
        private readonly SwaggerUIOptions options;

        public KubeSwaggerUiMiddleware(IWebHostEnvironment hostingEnv, ILoggerFactory loggerFactory,
            IOptionsSnapshot<SwaggerUIOptions> options)
        {
            this.hostingEnv = hostingEnv;
            this.loggerFactory = loggerFactory;
            this.options = options.Value;
        }

        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            var m = new SwaggerUIMiddleware(next, this.hostingEnv, this.loggerFactory, this.options);
            await m.Invoke(context);
        }
    }

Create a class that implements IConfigureOptions

public class SwaggerUIOptionsConfigure : IConfigureOptions<SwaggerUIOptions>
    {
        private readonly ILogger<SwaggerUIOptionsConfigure> logger;
        private readonly SwaggerSharedOptions swaggerOAuth2Options;
        private readonly IKubeService kubeService;
        private readonly SwaggerForOcelotUIOptions swaggerOcelotOptions;

        public SwaggerUIOptionsConfigure(ILogger<SwaggerUIOptionsConfigure> logger,
            IOptionsSnapshot<SwaggerForOcelotUIOptions> swaggerOcelotOptions,
            IOptionsSnapshot<SwaggerSharedOptions> swaggerOAuth2Options,
            IKubeService kubeService)
        {
            this.logger = logger;
            this.swaggerOAuth2Options = swaggerOAuth2Options.Value;
            this.kubeService = kubeService;
            this.swaggerOcelotOptions = swaggerOcelotOptions.Value;
        }
        public void Configure(SwaggerUIOptions options)
        {
            options.ConfigObject = this.swaggerOcelotOptions.ConfigObject;
            options.DocumentTitle = this.swaggerOcelotOptions.DocumentTitle;
            options.HeadContent = this.swaggerOcelotOptions.HeadContent;
            options.IndexStream = this.swaggerOcelotOptions.IndexStream;
            options.OAuthConfigObject = this.swaggerOcelotOptions.OAuthConfigObject;
            options.RoutePrefix = this.swaggerOcelotOptions.RoutePrefix;

            options.OAuthClientId(this.swaggerOAuth2Options.ClientId);

            this.AddEndpoints(options);
        }

        private void AddEndpoints(SwaggerUIOptions options)
        {
            //Not really safe, but cant await here :(
            var services = this.kubeService.ListAsync().GetAwaiter().GetResult();
            var path = this.swaggerOcelotOptions.DownstreamSwaggerEndPointBasePath;

            // Clear the list of services before adding more
            options.ConfigObject.Urls = null;
            foreach (var service in services)
            {
                this.logger.LogDebug("Service {Name} has swagger enabled: {SwaggerEnabled}", service.Name, service.SwaggerEnabled);
                if (service.SwaggerEnabled)
                {
                    options.SwaggerEndpoint($"{path}/{service.Version}/{service.Name}", $"{service.SwaggerDisplay} - {service.Version}");
                }
            }
        }

    }

Then register both services

services.AddScoped<IConfigureOptions<SwaggerUIOptions>, SwaggerUIOptionsConfigure>();
services.AddScoped<KubeSwaggerUiMiddleware>();

And use the middleware instead the real swaggerUI middleware

app.UseMiddleware<KubeSwaggerUiMiddleware>();
Was this page helpful?
0 / 5 - 0 ratings