RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.67k stars 1.23k forks source link

Please provide equivalent configuration on v12.0.0 for obsolete UseSwagger() GenericSettings object #1760

Open OculiViridi opened 5 years ago

OculiViridi commented 5 years ago

After updating from v11.20.1 to v12.0.0 I'm now on a deprecated "limbo".

image

Here it is my current complete v11.20.1 code for configuration:

public static void UseMySwagger(this IApplicationBuilder app)
{
    string title = "My Company DCS Control API";
    string description = "My Company API for Core functionalities.";

    ControlApiConfiguration apiConfiguration = (ControlApiConfiguration)app.ApplicationServices.GetService(typeof(ControlApiConfiguration));
    Uri baseUri = new Uri(apiConfiguration.AuthEndpoint);

    app.UseSwagger(typeof(Startup).Assembly, config =>
    {
        config.GeneratorSettings.OperationProcessors.TryGet<ApiVersionProcessor>().IncludedVersions = new[] { "1.0" };
        config.GeneratorSettings.OperationProcessors.Add(new OperationSecurityScopeProcessor("oauth2"));
        config.GeneratorSettings.DocumentProcessors.Add(
            new SecurityDefinitionAppender("oauth2", new SwaggerSecurityScheme
            {
                Type = SwaggerSecuritySchemeType.OAuth2,
                Flow = SwaggerOAuth2Flow.Implicit,
                AuthorizationUrl = new Uri(baseUri, "connect/authorize").ToString(),
                Scopes = new Dictionary<string, string> { { "mycompany.core.v1", $"{title} - Full access" } }
            }));

        config.DocumentPath = "/v1.0.json";

        config.GeneratorSettings.Title = title;
        config.GeneratorSettings.Description = description;
        config.GeneratorSettings.Version = "1.0.0";
        config.PostProcess = document =>
        {
            document.Info.Contact = new SwaggerContact
            {
                Name = "My Company",
                Email = "info@mycompany.com",
                Url = "https://www.mycompany.com"
            };
        };
    });

    app.UseSwagger(typeof(Startup).Assembly, config =>
    {
        config.GeneratorSettings.OperationProcessors.TryGet<ApiVersionProcessor>().IncludedVersions = new[] { "2.0" };
        config.GeneratorSettings.OperationProcessors.Add(new OperationSecurityScopeProcessor("oauth2"));
        config.GeneratorSettings.DocumentProcessors.Add(
            new SecurityDefinitionAppender("oauth2", new SwaggerSecurityScheme
            {
                Type = SwaggerSecuritySchemeType.OAuth2,
                Flow = SwaggerOAuth2Flow.Implicit,
                AuthorizationUrl = new Uri(baseUri, "connect/authorize").ToString(),
                Scopes = new Dictionary<string, string> { { "mycompany.core.v2", $"{title} - Full access" } }
            }));

        config.DocumentPath = "/v2.0.json";

        config.GeneratorSettings.Title = title;
        config.GeneratorSettings.Description = description;
        config.GeneratorSettings.Version = "2.0.0";
        config.PostProcess = document =>
        {
            document.Info.Contact = new SwaggerContact
            {
                Name = "My Company",
                Email = "info@mycompany.com",
                Url = "https://www.mycompany.com"
            };
        };
    });

    app.UseSwaggerUi3(config =>
    {
        config.OAuth2Client = new OAuth2ClientSettings { ClientId = "core-api-swagger", AppName = $"{title} - Swagger" };

        config.SwaggerRoutes.Add(new SwaggerUi3Route("v1.0", "/v1.0.json"));
        config.SwaggerRoutes.Add(new SwaggerUi3Route("v2.0", "/v2.0.json"));
    });
}

I want to align my code with your updated specs and avoid stuck on deprecated stuff. :wink: I need to get an equivalent updated configuration to the above one that is currently implemented in my project. I read the NSwag v12.0.0 (Build 1032) link, but I wasn't able to find out where the GeneralSettings (or an equivalent new object/method) is located. Thank you!

RicoSuter commented 5 years ago
OculiViridi commented 5 years ago

After a few attempts... I'm back!

As pointed out in #1759, this is what I need to configure on NSwag:

Now the problems with the following code are:

  1. With the exact below code, when opening http://localhost:5001/swagger I get 404 error on v1.0-beta/swagger.json.

image

  1. If I remove the following lines from my UseSwaggerWithConfiguration extension method:
    config.SwaggerRoutes.Add(new SwaggerUi3Route("v1.0-beta", "v1.0-beta/swagger.json"));
    config.SwaggerRoutes.Add(new SwaggerUi3Route("v2.0-beta", "v2.0-beta/swagger.json"));

    I get the UI but with the No operations defined in spec! message.

image

The CODE

Startup

Here there're the Startup class methods (ConfigureServices and Configure)

public void ConfigureServices(IServiceCollection services)
{
    // Adding AspNetCore Versioning
    services.AddApiVersioning(options =>
    {
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1, 0, "beta"); // Correct?
        options.ReportApiVersions = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    });

    services.AddMvcCore(config =>
        {
            config.ReturnHttpNotAcceptable = true;
            config.Filters.Add<OperationCancelledExceptionFilterAttribute>();
        })
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
        // Adding AspNetCore Versioning
        .AddVersionedApiExplorer(options =>
        {
            options.GroupNameFormat = "VVV";
            options.AssumeDefaultVersionWhenUnspecified = true;
            options.DefaultApiVersion = new ApiVersion(1, 0, "beta"); // Correct?
            options.SubstituteApiVersionInUrl = true;
        })

    services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
        .AddIdentityServerAuthentication(options =>
            {
                options.ApiName = "mycompany.core.v1";
                options.Authority = _apiConfiguration.AuthEndpoint;
                options.RequireHttpsMetadata = !Environment.IsDevelopment();
            });

    services.AddCors(options =>
        {
            options.AddPolicy("default", policy =>
                policy.WithOrigins(_apiConfiguration.CorsOrigins)
                    .AllowAnyHeader()
                    .AllowAnyMethod());
        });

    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    });

    // Call to my custom extension method (see code below)
    services.AddSwaggerDocumentWithConfiguration();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // BasePath configuration loaded from JSON settings file
    app.UsePathBase(new PathString(_apiConfiguration.BasePath));

    app.UseCors("default");

    // Taken from your NSwag code samples
    app.UseForwardedHeaders(new ForwardedHeadersOptions
    {
        ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
    });

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseStatusCodePages();
        app.UseHsts();
        app.UseGlobalExceptionHandler();
    }

    // Call to my custom extension method (see code below)
    app.UseSwaggerWithConfiguration();
    app.UseMvc();
}

Current Configuration

These are my custom extension methods, used to group all the necessary configuration in one separate place.

public static void AddSwaggerDocumentWithConfiguration(this IServiceCollection services)
{
    ServiceProvider serviceProvider = services.BuildServiceProvider();
    ControlApiConfiguration apiConfiguration = serviceProvider.GetService<ControlApiConfiguration>();
    Uri baseUri = new Uri(apiConfiguration.AuthEndpoint);

    /* Correct?
     * Derived from previous v11.20.1 configuration
     * I set API versioned documents in this way.
     */
    services.AddSwaggerDocument(config =>
    {
        config.DocumentName = "1.0";
        config.ApiGroupNames = new[] { "1.0-beta" };
        config.RequireParametersWithoutDefault = false;

        config.DocumentProcessors.Add(
            new SecurityDefinitionAppender("oauth2", new SwaggerSecurityScheme
            {
                Description = "OAuth 2",
                Type = SwaggerSecuritySchemeType.OAuth2,
                Flow = SwaggerOAuth2Flow.Implicit,
                AuthorizationUrl = new Uri(baseUri, "connect/authorize").ToString(),
                TokenUrl = new Uri(baseUri, "connect/token").ToString(),
                Scopes = new Dictionary<string, string> { { "mycompany.core.v1", $"{title} - Full access" } }
            }));

        config.OperationProcessors.Add(new OperationSecurityScopeProcessor("oauth2"));

        config.Title = title;
        config.Description = description;
        config.Version = "1.0.0";
        config.PostProcess = document =>
        {
            document.Info.Contact = new SwaggerContact
            {
                Name = "My Company",
                Email = "info@mycompany.com",
                Url = "https://www.mycompany.com"
            };
        };
    });

    services.AddSwaggerDocument(config =>
    {
        config.DocumentName = "2.0";
        config.ApiGroupNames = new[] { "2.0-beta" };
        config.RequireParametersWithoutDefault = false;

        config.DocumentProcessors.Add(
            new SecurityDefinitionAppender("oauth2", new SwaggerSecurityScheme
            {
                Description = "OAuth 2",
                Type = SwaggerSecuritySchemeType.OAuth2,
                Flow = SwaggerOAuth2Flow.Implicit,
                AuthorizationUrl = new Uri(baseUri, "connect/authorize").ToString(),
                TokenUrl = new Uri(baseUri, "connect/token").ToString(),
                Scopes = new Dictionary<string, string> { { "mycompany.core.v2", $"{title} - Full access" } }
            }));

        config.OperationProcessors.Add(new OperationSecurityScopeProcessor("oauth2"));

        config.Title = title;
        config.Description = description;
        config.Version = "2.0.0";
        config.PostProcess = document =>
        {
            document.Info.Contact = new SwaggerContact
            {
                Name = "My Company",
                Email = "info@mycompany.com",
                Url = "https://www.mycompany.com"
            };
        };
    });
}

public static void UseSwaggerWithConfiguration(this IApplicationBuilder app)
{
    app.UseSwagger(config =>
    {
        config.PostProcess = (document, request) =>
        {
            if (request.Headers.ContainsKey("X-External-Host"))
            {
                document.Host = request.Headers["X-External-Host"].First();
                document.BasePath = request.Headers["X-External-Path"].First();
            }
        };
    });

    app.UseSwaggerUi3(config =>
    {
        config.OAuth2Client = new OAuth2ClientSettings
        {
            ClientId = "core-api-swagger",
            ClientSecret = "secret".ToSha256(),
            AppName = $"{title} - Swagger"
        };

        config.TransformToExternalPath = (internalUiRoute, request) =>
        {
            // The header X-External-Path is set in the nginx.conf file
            var externalPath = request.Headers.ContainsKey("X-External-Path") ? request.Headers["X-External-Path"].First() : "";
            return externalPath + internalUiRoute;
        };

        // Correct?
        config.SwaggerRoutes.Add(new SwaggerUi3Route("v1.0-beta", "v1.0-beta/swagger.json"));
        config.SwaggerRoutes.Add(new SwaggerUi3Route("v2.0-beta", "v2.0-beta/swagger.json"));
    });
}

Sample Controller

In the Get method I replaced the ProducesResponseType attribute with SwaggerResponse attribute, as you suggested. While in the List() I leaved the ProducesResponseType just to see the difference in the swagger UI.

[ApiController]
[Authorize]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
[ApiVersion("1.0-beta")]
[Produces("application/json")]
[SwaggerTag("Anomalies", Description = "Anomalies management")]
public class AnomaliesController : ControllerBase
{
    /// <summary>
    /// Find anomaly by ID
    /// </summary>
    /// <param name="id">ID of the anomaly to return</param>
    /// <remarks>Returns a single anomaly object</remarks>
    /// <response code="200">Successful operation</response>
    /// <response code="400">Invalid ID supplied</response>
    /// <response code="404">Anomaly not found</response>
    [HttpGet("{id:long}")]
    [SwaggerResponse(HttpStatusCode.OK, typeof(api.Anomaly), Description = "Successfull operation")]
    [SwaggerResponse(HttpStatusCode.BadRequest, typeof(api.Anomaly), Description = "Invalid ID supplied")]
    [SwaggerResponse(HttpStatusCode.NotFound, typeof(api.Anomaly), Description = "Anomaly not found")]
    public async Task<ActionResult<api.Anomaly>> Get(long id)
    {
        return Mapper.Map<api.Anomaly>(await DcsMediator.Send(new GetAnomalyByIdQuery() { Id = id }));
    }

    /// <summary>
    /// Find anomalies by specified parameters
    /// </summary>
    /// <param name="machineId">ID of the machine that need to be considered for filter</param>
    /// <param name="type">AnomalyType value that need to be considered for filter</param>
    /// <param name="status">AnomalyStatus value that need to be considered for filter</param>
    /// <remarks>Values of filter are considered in AND</remarks>
    /// <response code="200">Successful operation</response>
    /// <response code="400">Invalid filter values</response>
    [HttpGet]
    [ProducesResponseType(typeof(List<api.Anomaly>), (int)HttpStatusCode.OK)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    public async Task<ActionResult<List<api.Anomaly>>> List(long? machineId, AnomalyType? type, AnomalyStatus? status, CancellationToken cancellationToken)
    {
        return Mapper.Map<List<api.Anomaly>>(await DcsMediator.Send(
            new GetAnomalyListByFilterQuery() { MachineId = machineId, Status = status, Type = type, IncludeMachine = true }, cancellationToken));
    }
}

Inside the code, I added some "pin" comments like //Correct? where I'm not sure about. I hope that maybe, at last, this configuration could become a complex code sample for NSwag, useful for someone else too! :satisfied:

Thank you!

RicoSuter commented 5 years ago
OculiViridi commented 5 years ago

You need to use these two routes in UseSwaggerUi() otherwise it cannot find it (404) (or just let it automatically register the document routes)

So, my options are:

  1. Automatically register the document routes. How?
  2. Manually add these lines (are those paths correct based on my sample code?):
    config.SwaggerRoutes.Add(new SwaggerUi3Route("v1.0-beta", "v1.0-beta/swagger.json"));
    config.SwaggerRoutes.Add(new SwaggerUi3Route("v2.0-beta", "v2.0-beta/swagger.json"));

    What should be the values for new SwaggerUi3Route()? What values do they have to match?

About API versioning, I followed GitHub aspnet-api-versioning specs to set versioning in services.AddApiVersioning() and services.AddMvcCore().AddVersionedApiExplorer() like this:

// VVV: Major, optional minor version, and status --> (ie: /api/v2.0-Alpha/foo) <--
options.GroupNameFormat = "VVV";
--> options.DefaultApiVersion = new ApiVersion(1, 0, "beta"); <--

Using the VVV format and 1.0-beta value in [ApiVersion] tag, it's actually working and by using Postman to call:

http://localhost:5001/api/v1.0-beta/Anomalies/Get/4

I obtain a successfull request.

Anyway, since it is not really necessary to me, I also tried to remove the beta part, but I get same previously described errors: 1 (404) or 2 (No operations defined in spec!).

RicoSuter commented 5 years ago

Because UseSwagger implicitly adds the two document routes (as listed in the previous post), you need to use

config.SwaggerRoutes.Add(new SwaggerUi3Route("v1.0-beta", "swagger/1.0/swagger.json"));
config.SwaggerRoutes.Add(new SwaggerUi3Route("v2.0-beta", "swagger/2.0/swagger.json"));

AddVersionedApiExplorer registers api groups with the name of the registered version but it seems that the name is not "1.0-beta". @commonsensesoftware do you know the registered api group name for this case?

OculiViridi commented 5 years ago

I made some adjustments (see changes below) but I'm still not able to get documentation working. With this code in place I always get

No operations defined in spec!

What am I missing or doing wrong? :confused:

My custom extension methods for configuration

services.AddSwaggerDocument(config =>
{
    config.DocumentName = "v1.0";
    config.ApiGroupNames = new[] { "1.0" };
    // ...
    config.Version = "1.0.0";
    // ...
}

services.AddSwaggerDocument(config =>
{
    config.DocumentName = "v2.0";
    config.ApiGroupNames = new[] { "2.0" };
    // ...
    config.Version = "2.0.0";
    // ...
}

app.UseSwaggerUi3(config =>
{
    // ...
    // Without the starting '/' I get 404
    config.SwaggerRoutes.Add(new SwaggerUi3Route("v1.0", "/swagger/v1.0/swagger.json"));
    config.SwaggerRoutes.Add(new SwaggerUi3Route("v2.0", "/swagger/v2.0/swagger.json"));
}

Startup

services.AddApiVersioning(options =>
{
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
});

.AddVersionedApiExplorer(options =>
{
    // VVV: Major, optional minor version, and status (ie: /api/v2.0-Alpha/foo);
    options.GroupNameFormat = "VVV";
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.SubstituteApiVersionInUrl = true;
})

Controller

[ApiController]
[Authorize]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
[ApiVersion("1.0")]
[Produces("application/json")]
public class AnomaliesController : ControllerBase
{ 
    // ...
}
RicoSuter commented 5 years ago

It seems that your ApiGroupNames are wrong/do not exist. Try to inject the IApiDescriptionGroupCollectionProvider interface into one of your controller, set a breakpoint and inspect the groups so that you know the existing groups...

OculiViridi commented 5 years ago

@RSuter Yes, it really seems that no ApiGroup is present.

image

So, could it be a problem related only to the configuration set with AddApiVersioning() and/or AddVersionedApiExplorer() extension methods in the Startup class?

RicoSuter commented 5 years ago

Yep, its probably a problem with a config in one of these two... check ou my sample and work from this to your solution

OculiViridi commented 5 years ago

Looking at your example, I found some differences with my Startup code.

Yours

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddApiVersioning(options =>
    {
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    })
    .AddMvcCore()
    .AddVersionedApiExplorer(options =>
    {
        options.GroupNameFormat = "VVV";
        options.SubstituteApiVersionInUrl = true;
    });

    // ...
}

Mine

public void ConfigureServices(IServiceCollection services)
{
    services.AddApiVersioning(options =>
    {
        options.AssumeDefaultVersionWhenUnspecified = true;
        //options.DefaultApiVersion = new ApiVersion(1, 0);
        //options.ReportApiVersions = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    });

    services.AddMvcCore()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
        .AddVersionedApiExplorer(options =>
        {
            // See: https://github.com/Microsoft/aspnet-api-versioning/wiki/Version-Format#custom-api-version-format-strings
            options.GroupNameFormat = "VVV"; // VVV: Major, optional minor version, and status (ie: /api/v2.0-Alpha/foo);
            //options.AssumeDefaultVersionWhenUnspecified = true;
            //options.DefaultApiVersion = new ApiVersion(1, 0);
            options.SubstituteApiVersionInUrl = true;
        });
}

Following this article, I setup my Web API project like you see in my code, so without using the AddMvc() method, but instead using just AddMvcCore().

From the article:

So when using AddMvcCore() we have to add everything by ourselves. This means, that we only have in our application what we really want and for example do not include the razor functionality which we do not need anyway.

In your code instead, you're using both methods. Is there any specific reason why you use them both?

If I understood correctly what the article says, the 2 methods are something like "mutual exlusive" at least at logical level (no compilation errors actually appears if you're using both like you did). So I think they're intended to be used as an alternative to each other. Am I wrong?

I also already tried to put the AddApiVersioning() after the AddMvcCore(), but nothing changes and no new errors appears too.

RicoSuter commented 5 years ago

In the sample it works:

image

I'm really not an expert with API versioning and dont know all the required settings to make this work. Maybe @commonsensesoftware can help you here?

In your code instead, you're using both methods. Is there any specific reason why you use them both?

That's probably a mistake...

commonsensesoftware commented 5 years ago

@OculiViridi I don't believe you've indicated which version of API versioning you're using. If it's 2.x, there's a chance that the order of service registration may be causing you grief. This has been remedied in 3.0+. I would recommend that you register AddMvc() or AddMvcCore() first. In addition, you have to also call AddApiExplorer() when you use AddMvcCore(). AddMvc() automatically does that. This has also been corrected in 3.0. That's almost certianly why you aren't seeing an results. The API Explorer in ASP.NET Core does most of the work. The API Versioning extensions merely collates the results such that they are grouped by API version.

In terms of matching up the groups to Swagger documents, the expectation is that they will match by API version. The default behavior will be to use the result of ApiVersion.ToString(). You have full control over how you want things to be formatted using the GroupNameFormat, which indicates the format that should be used in ApiVersion.ToString(IFormatProvider,string). I notice that you're using the built in format code VVV. This is not wrong; however, be aware that this does not include the v character. The v is not part of the API version. If you want or need to include the v in the group name, you can use the format:

options.GroupNameFormat = "'v'VVV";

It's also worth noting that the configuration for your Swagger documents seem to be nearly identical save the version information. You can use the IApiVersionDescriptionProvider provided by API versioning to enumerate all of the defined API versions in your application to build this information with a simple loop. You can use the various, supported format codes for the API version to achieve the different forms you use in your documentation. You can see an example of this at work here. This service will also do the work of determining whether an API version is deprecated for you. If all the APIs for a given version are deprecated, then the entire API version is considered deprecated too.

I happy to answer any other questions to help you get unblocked.

OculiViridi commented 5 years ago

@commonsensesoftware Thank you for joining the conversation!

I don't believe you've indicated which version of API versioning you're using.

I'm on .NET Core 2.1 (2.1.6).

In addition, you have to also call AddApiExplorer() when you use AddMvcCore()

Done! I removed it when introduced the AddVersionedApiExplorer(). Now it's back in place. As suggested by @RSuter, I can see the API Groups (1, 2) in the IApiDescriptionGroupCollectionProvider object using the debugger.

image

I notice that you're using the built in format code VVV. This is not wrong; however, be aware that this does not include the v character. The v is not part of the API version.

I'm using the VVV format and the v string is added by the attribute:

[Route("api/v{version:apiVersion}/[controller]/[action]")]

In fact the following piece of my code has always been the same and with NSwag v11.20.1 everything was working and I was able to succesfully call:

/api/v1.0/Anomalies/Get/1

My new idea is to use the version according to the /api/v1.0-beta/Anomalies/Get/1 format. But now, even after the below changes, I'm in trouble with swagger documents also using my original previous simplest /api/v1.0/MyController/Action format.

You can use the IApiVersionDescriptionProvider provided by API versioning to enumerate all of the defined API versions in your application to build this information with a simple loop.

Thanks! I will seriously take it into consideration, as it is certainly a cleaner approach, once I have managed to make swagger work again.

Anyway, with Swagger I'm still on

No operations defined in spec!

What's still wrong in my configuration? Now API Groups are in place. Is there still something to fix in my Startup configuration or where else? Please take a look at my adjusted code.

Startup

services.AddMvcCore()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
    .AddApiExplorer()
    .AddVersionedApiExplorer(options =>
    {
        options.GroupNameFormat = "VVV"; // VVV: Major, optional minor version, and status (ie: /api/v2.0-Alpha/foo);
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1, 0);
        options.SubstituteApiVersionInUrl = true;
    })
    .AddAuthorization()
    .AddJsonFormatters()
    .AddMvcLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization()
    .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<MachineCreateModelValidator>());

services.AddApiVersioning(options =>
{
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
});

Custom extension methods used for configuration of NSwag

services.AddSwaggerDocument(config =>
{
    config.DocumentName = "v1.0";
    config.ApiGroupNames = new[] { "1.0" };
    // ...
    config.Version = "1.0.0";
    // ...
}

services.AddSwaggerDocument(config =>
{
    config.DocumentName = "v2.0";
    config.ApiGroupNames = new[] { "2.0" };
    // ...
    config.Version = "2.0.0";
    // ...
}

app.UseSwaggerUi3(config =>
{
    // ...
    // Without the starting '/' I get 404
    config.SwaggerRoutes.Add(new SwaggerUi3Route("v1.0", "/swagger/v1.0/swagger.json"));
    config.SwaggerRoutes.Add(new SwaggerUi3Route("v2.0", "/swagger/v2.0/swagger.json"));
}

Controller

[ApiController]
[Authorize]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
[ApiVersion("1.0")]
[Produces("application/json")]
public class AnomaliesController : ControllerBase
{ 
    // ...
}

Also, another thing that's not working is the DefaultApiVersion. With the current code, using Postman, I can succesfully call http://localhost:5001/api/v1.0/Anomalies/Get/1 and get the expected result. But, if I'm not wrong, I should also be able to call http://localhost:5001/api/Anomalies/Get/1 (without the version specified) and being routed automatically to /v1.0 that's the default version. Actually I get empty response. With the debugger activated I don't reach the controller. See below image of Postman result.

image

What's the problem here?

commonsensesoftware commented 5 years ago

The first issue appears to be a mismatch on the group names. Remember that the VVV format uses an optional minor version. This means that the version 1.0 will be formatted as 1. This behavior can combined with the literal v character to produce v1, which is a common desire. Remember that v is not part of the API version. Yes, you have it in your route template, but it's outside the route constraint, meaning it's just a literal character. Now, your NSwag configuration says that the group name is "1.0" and "2.0". This does not match the formatted values "1" or "2". This is likely why you don't see any operations generated. Consider using the format codes VV or VVVV to match these strings. You can also change your group names to be "1" and "2" respectively.

Since you've elected to version by URL segment, the DefaultApiVersion is going to work a little differently. It is not possible to have optional or default values in the middle of a route template. This is how routing in ASP.NET Core (and probably any other framework) works. The recommended way to make this work is to include double routes for the default route. For example, add an additional [Route("api/[controller]/[action]")]. This template does not include a route constraint, which is where the API version would be derived from using this method. As you've allowed no API version to be specified and the value cannot be derived from the route parameters, the API version will be assumed to be the value of DefaultApiVersion. I hope that clears things up.

OculiViridi commented 5 years ago

Finally, I'm now able to get my Swagger docs!

@commonsensesoftware As you said,

Now, your NSwag configuration says that the group name is "1.0" and "2.0". This does not match the formatted values "1" or "2".

and, by just changing the GroupNameFormat to VVVV format, I'm now able to list all the controllers in the docs.

By the way, there's still a couple of issues...

1. Swagger paths issue

My actual configuration is (except for the GroupNameFormat now set to VVVV) exactly the same reported in my previous post here above. So please take it as reference code.

The automatically generated Swagger paths for methods of controller, now include the /v1/ path part. However, I expected it to be /v1.0/... why is it so? Since I set the VVVV format, I also tried by setting version as v1.0-beta. It gives me /v1-beta/ instead of /v1.0-beta/.

image

2. Default API version issue

I tried with your suggestion

The recommended way to make this work is to include double routes for the default route. For example, add an additional [Route("api/[controller]/[action]")].

and added

[Route("api/v{version:apiVersion}/[controller]/[action]")]
[Route("api/[controller]/[action]")]

But, doubling the routes, doubles (duplicates) methods on Swagger docs... see image below. @RSuter Is there another way to do that or a specific setting to avoid duplicates?

image

I think it could be useful to clarify what are the correspondences of values that must be taken into consideration to make things work properly.

Till now I understand that:

Is there any other match to consider that I'm missing?

commonsensesoftware commented 5 years ago

Here's some additional clarification. Keep in mind that the ApiVersion is a formal type and not a magic string. That said, in context of documentation there are several pieces of configuration. For API versioning, groups are collated by API version. I would think that it should fairly obvious that config.ApiGroupNames and the result of formatting using the GroupNameFormat must match. I think there may also be some confusion between the format used for grouping and the format used for substitution, which may not be the same. You may want to review the meaning of each of the options.

Swagger Path Issues

GroupNameFormat has no default value, which results in the ApiDescription.GroupName being the result of ApiVersion.ToString(). The choice to change it is really about how you want it to match up to NSwag and how NSwag might use this value. For example, it's completely reasonable for NSwag to build a list of documents using the distinct list of ApiDescription.GroupName values. In such a scenario, you may want to control how that value is rendered as it may surface in route paths or other parts of the Swagger UI.

The second part is how you want the API version to be formatted when it is substituted into the corresponding route template. This does not need to be the same as the format used for group names. This is controlled using the SubstitutionFormat whose default value is VVV as that tends to be the most common format used in the URL segment method. The SubstitutionFormat is what affects how the value looks in the final URL template. This means you want your configuration something like:

.AddVersionedApiExplorer(options =>
{
  options.GroupNameFormat = "VVVV";
  options.SubstitutionFormat = "VVVV";
  options.SubstituteApiVersionInUrl = true;
})

Default API Version Issue

I'm not familiar with how NSwag extensibility works, but I assume there is a way to customize Swagger document generation. There should be a way to filter out these operations so that entries are not generated for them. Technically, everything is working the way it's expected to. You really do have two different routes for the same API version.

Another approach is to use a custom IApiDescriptionProvider and register that with ASP.NET Core. You'll want your provider to run last by configuring the Order property. When your provider runs, you can look for ApiDescription results that have the path you don't want and remove them. Using this approach, NSwag will simply never see those ApiDescription instances.

RicoSuter commented 5 years ago

Is there another way to do that or a specific setting to avoid duplicates?

I think you can implement a custom operation processor and filter them out.

RicoSuter commented 5 years ago

Nginx/reverse proxy users, please review: https://github.com/RicoSuter/NSwag/pull/2196