domaindrivendev / Swashbuckle.AspNetCore

Swagger tools for documenting API's built on ASP.NET Core
MIT License
5.23k stars 1.31k forks source link

Dynamically Changing the Swagger UI Endpoints at Runtime #1093

Open kieronlanning opened 5 years ago

kieronlanning commented 5 years ago

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.

domaindrivendev commented 5 years ago

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

kieronlanning commented 5 years ago

@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!

majdisorder commented 4 years ago

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?

majdisorder commented 4 years ago

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?

sergiomcalzada commented 4 years ago

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>();
LetMeSleepAlready commented 2 years ago

Bit late to the discussion but...

I just solved this in the following way:


public class SwaggerEndpointEnumerator : IEnumerable<UrlDescriptor>
        {
            public IEnumerator<UrlDescriptor> GetEnumerator()
            {
                yield return new UrlDescriptor { Name = "Your swagger name here", Url = "Your URL here" };
            }

            IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
        }

Then in Configure()

app.UseSwaggerUI(c =>
            {
                c.ConfigObject.Urls = new SwaggerEndpointEnumerator();
            });

The GetEnumerator method will be hit every time you reload the UI.

I assume that it is clear enough that you can pretty much do whatever you want in the GetEnumerator method. I didnt need DI so your mileage might vary.

guillaume86 commented 2 years ago

That's a pretty clever solution with the IEnumerable :).

For a general solution I think the standard way of doing it is injecting an IOptionMonitor<> in place of the IOptions<> in the middleware and getting monitor.CurrentValue at the start of each request. I did it in my project by forking the middleware class and it seems to work fine. It's not perfect because CreateStaticFileMiddleware is called in the ctor so RoutePrefix is not really variable and I wouldn't submit a PR without that.

AndrewTriesToCode commented 1 year ago

Related to this, I have a multitenant library that allow per-tenant options. I understand that the swagger ui middleware locks in options at the time of pipeline definition -- either from the globally registered IOptionsSnapshot<SwaggerUIOptions> or passing an options instance.

Question: where is the SwaggerUIOptions option configuration registered? In the snippet below it clearly is registered as GetRequiredService returns it but in searching the repo I can't find anywhere where the options are rendered. https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/8f363f7359cb1cb8fa5de5195ec6d97aefaa16b3/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs#L33

I would reiterate the importance of this issue. Locking in the options value of time of app start prevents the use of IOptionsMonitor to get real time changed options. It also prevents the ability for per-tenant options in my library which can work with IOptions, IOptionsSnapshot, or IOptionsMonitor.

Ideally the middleware would inject the IOptions or similar instance into the invoke method and the options extracted at that moment. I understand this has an overhead of DI but the flexibility offered it a good tradeoff in my opinon.

Thanks!

mgh9 commented 7 months ago

bounce

Great solution, but to me, the operations'/path' URL of swagger document won't set relative to the reverse proxy and are the main APIs address. I tried to modify the operations path in the PreSerializeFilters but because I set a value for ConfigObject.Urls, the UseSwagger middleware won't hit, I don't know why