domaindrivendev / Swashbuckle.AspNetCore

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

CLI and Web endpoint produce different swagger.json outputs when routing convention is configured through an IApplicationModelConvention is delayed #1957

Open maurei opened 3 years ago

maurei commented 3 years ago

I'm a maintainer of the JsonApiDotNetCore library (JADNC) and working on an OpenAPI Specification integration using Swashbuckle.

In JADNC we configure the routing through the use of an IApplicationModelConvention. We register our routing convention not in the ConfigureServices(IServiceCollection) method but in the Configure(IApplicationBuilder) method of the Startup. Here is the gist of that:

public class Startup
{
    private static Action<MvcOptions> _postMvcOptionsConfiguration;
    public void ConfigureServices(IServiceCollection services)
    {
        /* other code ommitted for brevity */
        services.AddMvcCore().AddMvcOptions(options =>
        {
            _postMvcOptionsConfiguration?.Invoke(options);
        }).AddApiExplorer();
        services.AddSwaggerGen(options => { /* ... */ })
        /* other code ommitted for brevity */

    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {

        /* other code ommitted for brevity */

        // Here we can retrieve any service and register it as a convention which is what we do in JADNC, but keeping the example simple here
        // var routingConvention = app.ApplicationServices.GetService<CustomRoutingConvention>();
        _postMvcOptionsConfiguration = options => options.Conventions.Insert(0, new CustomRoutingConvention());

        app.UseSwagger(options =>
        {
            options.RouteTemplate = "docs/{documentName}/openapi.json";
        });
        /* other code ommitted for brevity */
    }
}

This works fine when accessing the OAS document on the docs/{documentName}/openapi.json endpoint. However, when generating the OAS document using dotnet swagger tofile I noticed the Configure(IApplicationBuilder) isn't being called, and as a result the desired routes don't make it to the document.

Eg the following difference will arise (snippet):

  "paths": {
    "/api/v1/articles/{id}": {
      "get": {
        "tags": [
          "Articles"
        ],
        "responses": {
          "200": {
            "description": "Success"
          }
        }
      }

From CLI:

  "paths": {
    "/{id}": {
      "get": {
        "tags": [
          "Articles"
        ],
        "responses": {
          "200": {
            "description": "Success"
          }
        }
      }

I've added a minimal repro case

For JADNC it's undesired to move the registration of the routing convention to ConfigureServices(IServiceCollection) because it depends on services that are registered in the DI container. See JsonApiRoutingConvention.

domaindrivendev commented 3 years ago

You can tell the CLI tool to use an identical host setup as your application with the following approach:

https://github.com/domaindrivendev/Swashbuckle.AspNetCore#use-the-cli-tool-with-a-custom-host-configuration

maurei commented 3 years ago

Thanks for the quick response.

I tried that and ensured the SwaggerHostFactory is actually called by the CLI, but Startup.Configure(IApplicationBuilder ) still isn't called. I've updated the repo to demonstrate this.

domaindrivendev commented 3 years ago

My bad - you're absolutely correct. With it's current implementation, the CLI tool builds an IHost or IWebHost and corresponding service container, and then uses that to retrieve an instance of ISwaggerProvider, but falls short of actually spinning up the application behind the scenes, and as a result (and something I'm only learning now) does not invoke the startup Configure method. I need to dig deeper to find an alternative approach that works - I'd prefer not have to actually spin up the app behind the scenes but it's starting to look like that may be neccessary in some shape or form.

maurei commented 3 years ago

Thanks for the elaboration. I'm looking into a workaround along the direction of executing the bit of code earlier that normally runs in Startup.Configure. Ideally I'd do this only when the code is being called by Swashbuckle CLI. Is there some environment variable being set that allows me to detect this?

gregsdennis commented 2 years ago

Is there any traction on this? I have other customizations that I need to include in the CLI-generated document.

Basically, this needs to be invoked:

app.UseSwagger(x =>
{
    x.PreSerializeFilters.Add(SwaggerDocCustomizer.ModifySwaggerDoc);
});
spaasis commented 2 years ago

UPDATE: I had a misconfiguration - using minimal hosting mode works (at least for my case). It does seem to require starting the app however...

Original: Program.cs

..
var app = builder.Build();
app
    ...
    .UseEndpoints(b => {
        b.MapVersionedODataRoute("odata", "api/odata", modelBuilder).Select().Count(); 
        //every /odata/ route and schema is excluded from the generated swagger.json although these lines are run
    });
Xitric commented 2 years ago

@domaindrivendev Just tuning in on this discussion to ensure that we understand the implications of any changes that may or may not be underway.

We are actually really happy that the CLI does not invoke the startup Configure method. In our setup, we have an appsettings.json file that does not in itself allow for the CLI to build the service collection due to missing configuration. We thus use appsettings.Development.json to run the Swashbuckle CLI, by setting ASPNETCORE_ENVIRONMENT=Development. However, this mode also causes our startup Configure method to seed our databases with development related data, for ease of use. We wouldn't want that to happen when building the swagger.json.

In this case, I guess we would need to maintain two sets of app settings files, such as appsettings.Development.json and appsettings.Swagger.json, or would it be possible to use a cleverer approach (similarly to what @maurei is asking for)?

bkoelman commented 1 year ago

Entity Framework faced the same problem for running migrations. How they solved that is to start your app, but first subscribe to runtime-level events. The event handler terminates the app after services have been registered and the needed info has become available. It's all described at https://andrewlock.net/exploring-dotnet-6-part-5-supporting-ef-core-tools-with-webapplicationbuilder/.