Aller au contenu

Scalar

De Banane Atomic

Links

ASP.NET Core 9 integration

dotnet add package Scalar.AspNetCore
Program.cs
builder.Services.AddOpenApi();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    // add the following lines
    app.MapScalarApiReference(options =>
    {
        options
            .WithTitle("MyApp API Reference")
            .WithTheme(ScalarTheme.Solarized)
            .WithClientButton(false)
            .WithDocumentDownloadType(DocumentDownloadType.Json);
            //.WithDefaultOpenAllTags(true)
    });
}

URL: http://localhost:5000/scalar http://localhost:5000/openapi/v1.json

Properties/launchSettings.json
{
  "profiles": {
    "http": {
      "launchBrowser": true,
      "launchUrl": "http://localhost:5000/scalar",
      "applicationUrl": "http://localhost:5000"
    }
  }
}

Configure OpenApi

OpenApiConfiguration.cs
public static class OpenApiConfiguration
{
    public static IMvcBuilder ConfigureOpenApi(this IMvcBuilder builder)
    {
        builder.Services.AddOpenApi();
        return builder;
    }

    public static void MapScalar(this WebApplication app)
    {
        if (!app.Environment.IsProduction())
        {
            app.MapOpenApi();
            app.MapScalarApiReference(options =>
            {
                options
                    .WithTitle("My App API Reference")
                    .WithTheme(ScalarTheme.Solarized)
                    .WithClientButton(false)
                    .WithDocumentDownloadType(DocumentDownloadType.Json);
            });
        }
    }
}
Program.cs
builder.Services.ConfigureOpenApi();

app.MapScalar();

Customize the document

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<CustomOpenApiDocumentTransformer>();
});

private sealed class CustomOpenApiDocumentTransformer : IOpenApiDocumentTransformer
{
    public Task TransformAsync(
        OpenApiDocument document,
        OpenApiDocumentTransformerContext context,
        CancellationToken cancellationToken)
    {
        document.Info = new()
        {
            Title = "My App",
            Version = $"{context.DocumentName}.0.0",
            Description = "My description"
        };

        // remove the servers property
        document.Servers = [];

        document.Servers = document.Servers
            .Select(server =>
            {
                server.Url = server.Url.Replace("http:", "https:");
                return server;
            })
            .ToList();

        return Task.CompletedTask;
    }
}

Handle Enum as String

builder.Services.AddOpenApi(options =>
{
    options.AddSchemaTransformer<EnumAsStringSchemaTransformer>();
});

private sealed class EnumAsStringSchemaTransformer : IOpenApiSchemaTransformer
{
    public Task TransformAsync(
        OpenApiSchema schema,
        OpenApiSchemaTransformerContext context,
        CancellationToken cancellationToken)
    {
        if (context.JsonTypeInfo.Type.IsEnum)
        {
            schema.Type = "string";
            schema.Enum.Clear();

            foreach (var name in Enum.GetNames(context.JsonTypeInfo.Type))
                schema.Enum.Add(new OpenApiString(name));
        }

        return Task.CompletedTask;
    }
}

OAuth2

OpenApiConfiguration.cs
public static class OpenApiConfiguration
{
    private const string oAuth2Scheme = "OAuth2";

    public static IMvcBuilder ConfigureOpenApi(this IMvcBuilder builder)
    {
        builder.Services.AddOpenApi(options =>
        {
            options.AddDocumentTransformer<OAuth2OpenApiDocumentTransformer>();
        });
    }

    public static void MapScalar(this WebApplication app, AzureAdConfiguration configuration)
    {
        app.MapScalarApiReference(options =>
        {
            options
                .AddPreferredSecuritySchemes(oAuth2Scheme)
                .AddAuthorizationCodeFlow(oAuth2Scheme, flow =>
                {
                    flow.ClientId = configuration.SwaggerClientId;
                    flow.Pkce = Pkce.Sha256;
                    flow.SelectedScopes = [$"api://{configuration.ClientId}/ReadWrite.All"];
                })
                .WithPersistentAuthentication(true):
        });
    }
}
OAuth2OpenApiDocumentTransformer.cs
public class OAuth2OpenApiDocumentTransformer(IConfiguration configuration) : IOpenApiDocumentTransformer
{
    public Task TransformAsync(
        OpenApiDocument document,
        OpenApiDocumentTransformerContext context,
        CancellationToken cancellationToken)
    {
        var azureAdConfiguration = configuration.Get<MyAppConfiguration>().AzureAd;

        document.Components ??= new OpenApiComponents();
        document.Components.SecuritySchemes.Add(oAuth2Scheme, new OpenApiSecurityScheme
        {
            Type = SecuritySchemeType.OAuth2,
            Flows = new OpenApiOAuthFlows
            {
                AuthorizationCode = new OpenApiOAuthFlow
                {
                    AuthorizationUrl = new Uri($"https://login.microsoftonline.com/{azureAdConfiguration.TenantId}/oauth2/v2.0/authorize"),
                    TokenUrl = new Uri($"https://login.microsoftonline.com/{azureAdConfiguration.TenantId}/oauth2/v2.0/token"),
                    Scopes = new Dictionary<string, string>
                    {
                        { $"api://{azureAdConfiguration.ClientId}/ReadWrite.All", "API access" }
                    }
                }
            }
        });

        return Task.CompletedTask;
    }
}

Multiple versions

OpenApiConfiguration.cs
public static class OpenApiConfiguration
{
    public static IMvcBuilder ConfigureOpenApi(this IMvcBuilder builder)
    {
        builder.Services.AddOpenApi("v1");
        builder.Services.AddOpenApi("v2");

        return builder;
    }
}
Program.cs
app.MapScalarApiReference(options =>
{
    options
        .AddDocument("v1", "Version 1")
        .AddDocument("v2", "Version 2");
});