Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swagger file generated in cli missing paths, components (Swashbuckle.AspNetCore.Cli 5.1.0) #1580

Closed
kepalas opened this issue Mar 19, 2020 · 17 comments
Labels
p2 Medium priority

Comments

@kepalas
Copy link

kepalas commented Mar 19, 2020

During the build process we are trying to generate the swagger.json file.
We are using the following command:

dotnet swagger tofile .\bin\Debug\netcoreapp3.1\myWebService.dll v1

The output that gets generated:

{
  "openapi": "3.0.1",
  "info": {
    "title": "myWebService",
    "description": "myWebService",
    "contact": {
      "name": "John Doe",
      "email": "john.doe@example.com"
    },
    "license": {
      "name": "MIT",
      "url": "https://opensource.org/licenses/MIT"
    },
    "version": "1.0"
  },
  "paths": { },
  "components": { }
}

As you can see, the paths and components are empty. There are no errors or warnings shown from the CLI tool.

However, if we take the swagger.json from the service (http://localhost:57016/swagger/v1/swagger.json), it includes all the relevant paths and components.
image

Can you please clarify how can we achieve the same output in CLI as from the web service?

@domaindrivendev
Copy link
Owner

I've no idea why this might happen - could you create a minimal project that repro's the issue and post to github. Without a simple repro, it's hard for me to troubleshoot.

@kepalas
Copy link
Author

kepalas commented Mar 22, 2020

I created an example project and uploaded it here: https://github.com/kepalas/Swashbuckle.Repro

@kepalas
Copy link
Author

kepalas commented Apr 2, 2020

Same issue with Swashbuckle.AspNetCore.Cli 5.2.1.
@domaindrivendev, any ideas why this is happening?

@domaindrivendev
Copy link
Owner

I've taken a quick look and I don't think the root cause lies in the Swashbuckle library. I believe it's related to the custom ApiExplorer that your injecting for OData (see https://github.com/kepalas/Swashbuckle.Repro/blob/master/WebApplication1/Startup.cs#L50). Swashbuckle relies heavily on the implementation of ApiExplorer registered with your application. This component surfaces an ApiDescription for every API action method, and then SB in turn uses this to generate a corresponding Swagger/OpenAPI Operation. When I stepped through your sample app, I could see that the ODataApiExplorer is NOT surfacing the required descriptions.

So, it might be worth digging deeper into this component. @commonsensesoftware any ideas on what might be going on here?

@commonsensesoftware
Copy link

@domaindrivendev, I don't fully know or understand the initialization steps that the CLI code path takes. Based on what @kepalas has demonstrated, things definitely went all the way through the ODataApiExplorer while running the site as normal. I'm guessing that ConfigureServices gets called. Is Configure also called?

I'll try to keep this succinct, but I think it's worth clarifying what happens on the API Versioning side for OData. Ultimately, it's a consequence of OData's design. The normal setup expects the following to happen in Configure:

app.UseMvc(routes => routes.MapVersionedODataRoutes("odata", null, builder.GetEdmModels()));

OData provides no intrinsic way to track or otherwise know which routes have been registered. In particular, there needs to be a way to know the pairing of route, EDM, and API version in order to support API exploration. API Versioning adds a service called IODataRouteCollection, which contains ODataRouteMapping instances. This collection is appended after every call to MapVersionedODataRoutes or MapVersionedODataRoute.

ActionDescriptor instances are required to build ApiDescription instances (as you know). The default OData implementation excludes all actions from ApiExplorer. API Versioning solves this with its own, custom IActionDescriptorProvider. There is also a custom IActionDescriptorChangeProvider, which is triggered for re-evaluation every time MapVersionedODataRoutes or MapVersionedODataRoute is invoked.

Sooo ... long explanation, not so short...

If MapVersionedODataRoutes or MapVersionedODataRoute is never called, then the ODataApiExplorer does not have the information required to explore OData actions. This is the only difference I can see thus far that would explain why things work while the web site is running, but not via the CLI.

Hopefully, that provides context and we can start whittling down to where the source of the problem lies.

@manisha201301
Copy link

I am having the same issue. Any work-arounds for this?

@domaindrivendev
Copy link
Owner

@kepalas @manisha201301 @commonsensesoftware revisiting this issue ...

I just cracked open the repro project again, and even used the following approach (https://github.com/domaindrivendev/Swashbuckle.AspNetCore#use-the-cli-tool-with-a-custom-host-configuration) to ensure the CLI tool runs in an identical host setup to the application itself, but still the same issue.

Have to say, I'm completely stumped at this point!!! So, any assistance to dig deeper and try figure it out would be greatly appreciated.

@commonsensesoftware
Copy link

Without walking through all of the code myself, it's hard to be sure. I spent some time looking at all the moving parts and it looks right.

A couple of questions:

  1. Do we know that CLI works with convention-based routing at all, even without OData?
    a. I didn't see any related unit or integration tests, but that doesn't mean they don't exist
    b. Endpoint routing has been the de fact for a while now, so if that is the issue, it's easy to understand how it was overlooked
  2. Are we sure that the callback in UseMvc was actually invoked?
    a. If a logging statement or something else is added to confirm its invocation, does it run?
  3. I found this test that suggests .Configure(_ => { }) needs to be called to register routes.

I couldn't find exactly where the code is (anymore), but I know that the Configure method typically defined in Startup.cs is just an arbitrary Delegate to a method called Configure. The infrastructure examines the type of each defined parameter, resolves it from DI, and then invokes the delegate dynamically with those parameters. I don't believe the method is even compiled to an invokable function because that happens just once.

If I have time, I'll try to dive in deeper. If UseMvc is being called, then I'm a bit stumped too. I suspect it's not, but I could be wrong. It might also be worth using TestHost or fork your own implementation (@domaindrivendev) so that you actually fully start and stop all of the hosting infrastructure. The test host doesn't actually host anything, but calling through IWebHost.StartAsync and IWebHost.StopAsync will run through all of the startup and teardown. From what I've seen so far, the code only runs through service initialization, which appears to be missing a few more required steps.

That's my observations so far. I hope that helps.

@domaindrivendev
Copy link
Owner

@commonsensesoftware a combination of your insights above and another unrelated-but-actually-related issue (see #1957) are leading me to the root cause ...

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.

@commonsensesoftware
Copy link

@domaindrivendev excellent! My recommendation as a starting point would be to checkout the Test Host. You can probably use it in its entirety, but if not, it should provide a lot of good pointers on hosting without hosting. The TestServer doesn't actually host any endpoints.

@domaindrivendev domaindrivendev added p2 Medium priority and removed waiting for response labels Jan 18, 2021
@RealGecko
Copy link

RealGecko commented Oct 26, 2022

Hello,

I'm not sure if it will help, but let's give it a try.

I had a similar problem, but with custom SwaggerHostFactory.CreateHost. The dotnet swagger tofile (...) returned incomplete swagger definition:

{                                                                                     
  "openapi": "3.0.1",                                                                 
  "info": {                                                                           
    "title": "(...)",                                         
    "version": "v1"                                                                   
  },                                                                                  
  "paths": { },                                                                       
  "components": {                                                                     
    "securitySchemes": {                                                              
      "Bearer": {                                                                     
        "type": "http",                                                               
        "description": "Bearer token for the API to authenticate incoming requests.", 
        "scheme": "bearer"                                                            
      }                                                                               
    }                                                                                 
  },                                                                                  
  "security": [                                                                       
    {                                                                                 
      "Bearer": [ ]                                                                   
    }                                                                                 
  ]                                                                                   
}

Everything worked just fine:

  • when I've accessed swagger-ui page
  • when I've manually called SwaggerHostFactory.CreateHost during application startup:
(...)
var host = SwaggerHostFactory.CreateHost();
var swaggerProvider = host.Services.GetRequiredService<ISwaggerProvider>();
(...)

To resolve this issue I had to set a value for application name, so finally my SwaggerHostFactory.CreateHost looks like that:

/// <summary>
/// Custom <see cref="IHost"/> factory used for API client generation.
/// </summary>
public class SwaggerHostFactory
{
    public static IHost CreateHost()
    {
        var builder = WebApplication.CreateBuilder(new WebApplicationOptions
        {
            ApplicationName = typeof(SwaggerHostFactory).Namespace // without it, actions won't be added to swagger file
        });

        builder.Services.AddControllers();
        builder.Services.AddEndpointsApiExplorer();
        
        builder.Services.AddSynergySwaggerGen(options =>
        {
            options.SwaggerDoc("v1", new() { Title = "...", Version = "v1" });
            (...)
        });
            
        var app = builder.Build();
        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint(...);
            (...)
        });
        app.MapControllers();

        return app;
    }
}

Problem might be related to Microsoft.Extensions.DependencyInjection.MvcCoreServiceCollectionExtensions.GetApplicationPartManager which initializes Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager and calls void PopulateDefaultParts(string entryAssemblyName), where entryAssemblyName is set to environment?.ApplicationName. It is called only for not empty, or null values.

private static ApplicationPartManager GetApplicationPartManager(IServiceCollection services, IWebHostEnvironment? environment)
{
    var manager = GetServiceFromCollection<ApplicationPartManager>(services);
    if (manager == null)
    {
        manager = new ApplicationPartManager();

        var entryAssemblyName = environment?.ApplicationName;
        if (string.IsNullOrEmpty(entryAssemblyName))
        {
            return manager;
        }

        manager.PopulateDefaultParts(entryAssemblyName);
    }

    return manager;
}

Why do I think so?

var partManager = app.Services.GetRequiredService<ApplicationPartManager>();
var feature = new ControllerFeature();
partManager.PopulateFeature(feature);

During normal runtime, feature.Controllers has records for each controller from my project, and during dotnet swagger tofile (...) it is empty,

Cheers,

@Jeanot-Zubler
Copy link

To resolve this issue I had to set a value for application name, so finally my SwaggerHostFactory.CreateHost looks like that:

This also seems to solve my problem. As far as I see, you need to set the ApplicationName to your AssemblyName. By default this equals the project name and (on windows) is not case sensitive.

@mroberts91
Copy link

I ran across this when using Swashbuckle.AspNetCore.Cli v6.4.0, in a net7.0 API project.
When using var builder = WebApplication.CreateBuilder();, this results in the following empty Open API document.

{
  "openapi": "3.0.1",
  "info": {
    "title": "My API",
    "version": "V1"
  },
  "paths": { },
  "components": { }
}

But to resolve this, simply pass args to the CreateBuilder method. I found that this results in a fully built Open API file.

var builder = WebApplication.CreateBuilder(args);

@Ducki
Copy link

Ducki commented Mar 8, 2023

This is still a problem in 6.5.0

@dosper7
Copy link

dosper7 commented Mar 29, 2023

Same here, I'm using .net 7 with minimal API's.

@1bberto
Copy link

1bberto commented Jan 11, 2024

facing the same issue with .net8

@martincostello
Copy link
Collaborator

Duplicate of #1957

@martincostello martincostello marked this as a duplicate of #1957 Apr 14, 2024
@martincostello martincostello closed this as not planned Won't fix, can't repro, duplicate, stale Apr 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
p2 Medium priority
Projects
None yet
Development

No branches or pull requests