Swagger

De Banane Atomic
Aller à la navigationAller à la recherche

Liens

NSwag

Installation

Powershell.svg
# pour vscode
dotnet add package NSwag.AspNetCore
# Add to the project file *.csproj:
#  <ItemGroup> 
#    <PackageReference Include="NSwag.AspNetCore" Version="11.17.15" />

Configuration

Configuration Web API

Program.cs
builder.Services.AddSwaggerDocument(configuration =>
{
    configuration.Title = "MyApp";
    configuration.Version = typeof(Program).Assembly.GetSimplifiedVersion();
});

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi3();
}

Configuration MVC

Startup.cs
using NJsonSchema;
using NSwag.AspNetCore;
using System.Reflection;

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMvc();

    // à ajouter avant app.UseSpa
    app.UseSwaggerUi(typeof(Startup).GetTypeInfo().Assembly, settings =>
    {
        settings.GeneratorSettings.DefaultPropertyNameHandling = PropertyNameHandling.CamelCase;
        settings.PostProcess = document =>
        {
            //document.Info.Version = "v1";
            document.Info.Title = "Test API";
            document.Info.Description = "A simple ASP.NET Core web API";
            //document.Info.TermsOfService = "None";
            document.Info.Contact = new NSwag.SwaggerContact
            {
                Name = "Nicolas",
                //Email = string.Empty,
                //Url = "https://twitter.com/spboyer"
            };
            /*document.Info.License = new NSwag.SwaggerLicense
            {
                Name = "Use under LICX",
                Url = "https://example.com/license"
            };*/
        };
    });

Problème avec IActionResult

NSwag utilise la réflexion pour obtenir le type de retour. Avec IActionResult il ne peut pas.

Csharp.svg
[HttpGet]
// utiliser SwaggerResponse
[SwaggerResponse(HttpStatusCode.OK, typeof(IReadOnlyList<ItemDto>))]
[SwaggerResponse(HttpStatusCode.BadRequest, typeof(void))]
// ou ProducesResponseType
[ProducesResponseType(typeof(IReadOnlyList<ItemDto>), StatusCodes.Status200OK)]
public IActionResult Get()

Swashbuckle

Swashbuckle installation

Already installed in .NET 7+

MyWebApi.csproj
<ItemGroup>
  <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
Program.cs
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger()
       .UseSwaggerUI();
}

Installation Old

Bash.svg
dotnet add package Swashbuckle.AspNetCore
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "EFWebApi", Version = "v1" });
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c => 
        {
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "EFWebApi v1");
            c.RoutePrefix = string.Empty;  // serve the Swagger UI at the app's root (http://localhost:<port>/)
        });
    }

Usage

Controllers/ItemController.cs
[ApiController]
[Route("[controller]")]
[Produces("application/json")]  // set the Media type
public class ItemController : ControllerBase
{
    [HttpGet]
    [ProducesResponseType(typeof(IEnumerable<Item>), StatusCodes.Status200OK)]  // set the status code and the return type
    public IActionResult Get() { /* ... */ }
}

XML documentation

& has to be escaped as &amp;
MyProject.csproj
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
Program.cs
builder.Services.AddSwaggerGen(
    options =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "My Web Api", Version = "v1" });

        // Set the comments path for the Swagger JSON and UI.
        var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
    });
}
Controllers/ItemController.cs
[Produces("application/json")]
public class MyController() : ControllerBase
{
    /// <summary>Get all the items.</summary>
    /// <remarks>
    /// Sample request:
    ///
    ///     GET /item
    /// </remarks>
    /// <response code="200">Returns all the items.</response>
    [HttpGet]
    public IEnumerable<Item> GetAll() { /* ... */ }

    /// <summary>Update an item.</summary>
    /// <param name="timeSeries">The new time series to add.</param>
    /// <remarks>
    /// Sample request:
    ///
    ///     PUT /item/9
    /// </remarks>
    [HttpPut("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)] // by default returning void or Task is associated to status code 200
    public Task UpdateAsync(int id, CreateUpdateItemQuery query) { /* ... */ }

Version

Program.cs
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();

// nuget package Asp.Versioning.Mvc.ApiExplorer
builder.Services
    .AddApiVersioning(/* ... */)
    .AddApiExplorer(
        options =>
        {
            options.GroupNameFormat = "'v'VVV";
            options.SubstituteApiVersionInUrl = true;
        });

app.UseSwagger();
app.UseSwaggerUI(
    options =>
    {
        var descriptions = app.DescribeApiVersions();
        foreach (var description in descriptions)
            options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
    });
ConfigureSwaggerOptions.cs
public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions<SwaggerGenOptions>
{
    public void Configure(SwaggerGenOptions options)
    {
        foreach (var description in provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
        }

        static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
            => new()
            {
                Title = "Power Analytics Model Management API",
                Version = description.ApiVersion.ToString(),
                Description = "API Description."
            };
    }
}

Document Filter

Program.cs
builder.Services.AddSwaggerGen(c =>
{
    c.DocumentFilter<MyDocumentFilter>();
});
MyDocumentFilter.cs
public class MyDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        // add new property
        swaggerDoc.Extensions.Add("propertyName", new OpenApiObject
        {
            ["propertyName"] = new OpenApiString("value"),
            ["propertyName"] = new OpenApiArray
            {
                { new OpenApiString("value") }
            }
        }
    }
}

Add HealthChecks endpoint

Program.cs
builder.Services.AddHealthChecks();

builder.Services.AddSwaggerGen(c =>
{
    c.DocumentFilter<HealthChecksDocumentFilter>();
});

var app = builder.Build();
app.MapHealthChecks("/health");
HealthChecksDocumentFilter.cs
public class HealthChecksDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        var pathItem = new OpenApiPathItem();
        var operation = new OpenApiOperation();
        operation.Tags.Add(new OpenApiTag { Name = "ApiHealth" });

        var properties = new Dictionary<string, OpenApiSchema>
        {
            { "status", new OpenApiSchema() { Type = "string" } },
            { "errors", new OpenApiSchema() { Type = "array" } }
        };

        var response = new OpenApiResponse();

        response.Content.Add("application/json", new OpenApiMediaType
        {
            Schema = new OpenApiSchema
            {
                Type = "object",
                AdditionalPropertiesAllowed = true,
                Properties = properties
            }
        });

        operation.Responses.Add("200", response);
        pathItem.AddOperation(OperationType.Get, operation);
        swaggerDoc.Paths.Add("/health", pathItem);
    }
}

Dark theme

Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseStaticFiles();
        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "EFWebApi v1");
            c.RoutePrefix = string.Empty;
            c.InjectStylesheet("/swagger-ui/dark-theme.css");
        });
    }
wwwroot/swagger-ui/dark-theme.css
.swagger-ui .topbar .download-url-wrapper .select-label select {
    border: 2px solid #89bf04;;
}

body {
    background-color: #303030;
}
.swagger-ui,
.swagger-ui .info .title,
.swagger-ui .opblock-tag,
.swagger-ui section.models h4,
.swagger-ui .opblock .opblock-summary-operation-id,
.swagger-ui .opblock .opblock-summary-path,
.swagger-ui .opblock .opblock-summary-path__deprecated,
.swagger-ui table thead tr td,
.swagger-ui table thead tr th,
.swagger-ui .parameter__name,
.swagger-ui .parameter__type,
.swagger-ui .response-col_status,
.swagger-ui .model-title,
.swagger-ui .model,
.swagger-ui .tab li {
    color: #f0f0f0;
}

.swagger-ui input[type="email"],
.swagger-ui input[type="file"],
.swagger-ui input[type="password"],
.swagger-ui input[type="search"],
.swagger-ui input[type="text"],
.swagger-ui textarea {
    background: #303030;
    color: #f0f0f0;
    border: 1px solid gray;
}

.swagger-ui .opblock .opblock-section-header {
    background-color: #1b1b1b;
}
.swagger-ui .opblock .opblock-section-header h4,
.swagger-ui .btn {
    color: #f0f0f0;
}

.swagger-ui input[disabled], .swagger-ui select[disabled], .swagger-ui textarea[disabled] {
    background-color: #303030;
    color: lightgray;
}

.swagger-ui select {
    background: #303030 url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="gray"><path d="M13.418 7.859a.695.695 0 01.978 0 .68.68 0 010 .969l-3.908 3.83a.697.697 0 01-.979 0l-3.908-3.83a.68.68 0 010-.969.695.695 0 01.978 0L10 11l3.418-3.141z"/></svg>') right 10px center no-repeat;
    color: #f0f0f0;
}

.swagger-ui .response-control-media-type--accept-controller select {
    border-color: #89bf04;
}
.swagger-ui .response-control-media-type__accept-message {
    color: #89bf04;
}

.arrow path {
    fill: gray;
}

.swagger-ui section.models .model-container {
    border: 1px solid #61affe;
}

.swagger-ui .model-toggle::after {
    background: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="gray"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>') 50% no-repeat;
}

.swagger-ui .download-contents {
    width: unset;
}

The same schemaId is already used

Occurs when you have 2 classes with the same name in different namespaces.
Fix it by using the full class name (with namspace) for the schema id.

Program.cs
builder.Services.AddSwaggerGen(options =>
{
    options.CustomSchemaIds(type => type.ToString());
});

Url

Url Resource
http://localhost:<port>/swagger swagger UI
http://localhost:<port>/swagger/v1/swagger.json swagger json

Ouvrir le navigateur sur swagger

Dans un projet Web API avec Visual Studio Code, ouvrir le navigateur sur la page swagger.

.vscode\launch.json
{
    "configurations": [
        {
            "serverReadyAction": {
                "action": "openExternally",
                "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)",
                "uriFormat": "%s/swagger"
            },
            // autre solution
            "launchBrowser": {
                "enabled": true,
                "args": "${auto-detect-url}",
                "windows": {
                    "command": "cmd.exe",
                    "args": "/C start ${auto-detect-url}/swagger/index.html?url=/swagger/v1/swagger.json#!/Items"
                }
            }

Paramètres optionnels

Swagger ne gère pas les paramètres optionnels s'ils font partie du chemin, il les considère comme des paramètres required.