.NET & C# Development · Lesson 18 of 92

Generate Beautiful API Docs with OpenAPI & Scalar

Built-In OpenAPI Support (.NET 9+)

Starting with .NET 9, Microsoft.AspNetCore.OpenApi is the first-party way to generate OpenAPI documents. No more reaching for Swashbuckle by default.

Bash
dotnet add package Microsoft.AspNetCore.OpenApi
dotnet add package Scalar.AspNetCore
C#
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, ct) =>
    {
        document.Info = new OpenApiInfo
        {
            Title = "Orders API",
            Version = "v1",
            Description = "Manage orders, customers, and products.",
            Contact = new OpenApiContact
            {
                Name = "API Support",
                Email = "support@example.com"
            }
        };
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();            // serves /openapi/v1.json
    app.MapScalarApiReference(); // serves /scalar/v1
}

app.Run();

Visit /scalar/v1 in development for the Scalar UI.


Scalar — A Better UI

Scalar is a modern, fast alternative to Swagger UI. It renders your OpenAPI spec with a clean UI, supports auth flows, and generates client code snippets in multiple languages.

C#
app.MapScalarApiReference(options =>
{
    options.Title = "Orders API";
    options.Theme = ScalarTheme.Purple;
    options.DefaultHttpClient = new(ScalarTarget.CSharp, ScalarClient.HttpClient);
    options.Authentication = new ScalarAuthenticationOptions
    {
        PreferredSecurityScheme = "Bearer"
    };
});

Available themes: Default, Alternate, Moon, Purple, Solarized, BluePlanet, Saturn, Kepler, Mars, DeepSpace, None.


XML Comments for Endpoint Documentation

XML doc comments on your request/response models and handlers flow directly into the OpenAPI spec.

Enable XML output in your .csproj:

XML
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

Register it:

C#
builder.Services.AddOpenApi(options =>
{
    options.AddSchemaTransformer((schema, context, ct) =>
    {
        // XML comments on models are picked up automatically
        return Task.CompletedTask;
    });
});

Document your models:

C#
/// <summary>Request to create a new order.</summary>
public record CreateOrderRequest(
    /// <summary>ID of the customer placing the order.</summary>
    int CustomerId,
    /// <summary>Line items in the order.</summary>
    List<OrderLineRequest> Lines);

/// <summary>A line item in an order.</summary>
public record OrderLineRequest(
    /// <summary>Product ID.</summary>
    int ProductId,
    /// <summary>Quantity ordered. Must be at least 1.</summary>
    int Quantity);

Document your endpoints:

C#
/// <summary>Get an order by ID.</summary>
/// <param name="id">The order ID.</param>
/// <response code="200">Returns the order.</response>
/// <response code="404">Order not found.</response>
[HttpGet("{id}")]
[ProducesResponseType<Order>(200)]
[ProducesResponseType<ProblemDetails>(404)]
public async Task<IActionResult> GetOrder(int id) { }

IOperationFilter — Custom Headers and Auth in Docs

An IOperationFilter lets you modify every operation in the OpenAPI document programmatically. The classic use case: add a security requirement to every endpoint that has [Authorize].

C#
public class BearerAuthOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var hasAuthorize = context.MethodInfo
            .GetCustomAttributes(true)
            .OfType<AuthorizeAttribute>()
            .Any()
            || (context.MethodInfo.DeclaringType?
                .GetCustomAttributes(true)
                .OfType<AuthorizeAttribute>()
                .Any() ?? false);

        if (!hasAuthorize) return;

        operation.Security ??= [];
        operation.Security.Add(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    }
                },
                Array.Empty<string>()
            }
        });
    }
}

Register it and add the Bearer scheme definition:

C#
builder.Services.AddSwaggerGen(options =>
{
    options.OperationFilter<BearerAuthOperationFilter>();

    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT",
        Description = "Enter your JWT token."
    });
});

Versioned API Docs

When you have multiple API versions, generate a separate OpenAPI document for each.

C#
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
});

builder.Services.AddOpenApi("v1", options =>
{
    options.AddDocumentTransformer((doc, ctx, ct) =>
    {
        doc.Info.Title = "Orders API v1";
        doc.Info.Version = "1.0";
        return Task.CompletedTask;
    });
});

builder.Services.AddOpenApi("v2", options =>
{
    options.AddDocumentTransformer((doc, ctx, ct) =>
    {
        doc.Info.Title = "Orders API v2";
        doc.Info.Version = "2.0";
        return Task.CompletedTask;
    });
});
C#
app.MapOpenApi("/openapi/{documentName}.json");

app.MapScalarApiReference(options =>
{
    options.Servers = [];
    options.AddDocument("v1", "/openapi/v1.json", "Orders API v1");
    options.AddDocument("v2", "/openapi/v2.json", "Orders API v2");
});

Securing the Docs in Production

Never expose your OpenAPI spec or Scalar UI in production without auth.

Option 1 — Environment guard (simplest)

C#
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

Option 2 — Require authorization in all environments

C#
app.MapOpenApi()
    .RequireAuthorization("DocsPolicy");

app.MapScalarApiReference()
    .RequireAuthorization("DocsPolicy");

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("DocsPolicy", policy =>
        policy.RequireRole("Developer", "Admin"));
});

Option 3 — Static API key via middleware

C#
app.UseWhen(
    ctx => ctx.Request.Path.StartsWithSegments("/scalar"),
    branch => branch.Use(async (ctx, next) =>
    {
        if (!ctx.Request.Headers.TryGetValue("X-Docs-Key", out var key)
            || key != builder.Configuration["DocsKey"])
        {
            ctx.Response.StatusCode = 401;
            return;
        }
        await next(ctx);
    }));

Option 1 is fine for most teams. Use option 2 or 3 if your staging environment is publicly reachable.


Minimal API OpenAPI Metadata

When using Minimal APIs, attach metadata directly to endpoints:

C#
app.MapGet("/orders/{id}", GetOrderById)
    .WithName("GetOrderById")
    .WithSummary("Get an order by ID")
    .WithDescription("Returns the full order including line items.")
    .WithTags("Orders")
    .Produces<Order>(200)
    .ProducesProblem(404)
    .ProducesProblem(401)
    .RequireAuthorization()
    .WithOpenApi(op =>
    {
        op.Parameters[0].Description = "The unique order identifier";
        return op;
    });

The .WithOpenApi() overload accepts a transformer delegate for one-off customizations that don't warrant a full IOperationFilter.