OpenAPI & Swagger
Key Points
- OpenAPI (formerly Swagger) is the standard JSON/YAML schema for describing HTTP APIs — endpoints, parameters, responses, security.
Microsoft.AspNetCore.OpenApi(.NET 9+) is the first-party generator. AOT-friendly. Replaces Swashbuckle for new code.MapOpenApi()publishes/openapi.json. Pair with Scalar or Swagger UI for an interactive doc page.- For Minimal APIs: use
TypedResultsand metadata methods (WithName,WithSummary,Produces) for accurate spec. - For MVC controllers:
[ProducesResponseType]and XML comments for the spec. - Generate clients from the OpenAPI spec via NSwag, kiota, or
dotnet openapifor type-safe client code.
Concepts (deep dive)
Built-in OpenAPI (.NET 9+)
builder.Services.AddOpenApi(); // first-party generator
var app = builder.Build();
app.MapOpenApi(); // publishes /openapi.json
app.MapScalarApiReference(); // optional: Scalar UI at /scalar/v1
The first-party generator: - Reads metadata from endpoints (Minimal APIs and controllers). - Outputs OpenAPI 3.x JSON. - AOT-friendly. - Faster than Swashbuckle.
Endpoint metadata for OpenAPI
app.MapGet("/orders/{id:int}",
async Task<Results<Ok<Order>, NotFound>> (int id, IRepo r) =>
await r.GetAsync(id) is { } o ? TypedResults.Ok(o) : TypedResults.NotFound())
.WithName("GetOrder")
.WithSummary("Retrieve an order by ID")
.WithDescription("Returns 404 if the order does not exist.")
.WithTags("Orders")
.Produces<Order>(200)
.Produces(404);
Produces<T> is redundant when Results<...> is used — the typed result derives the schema. Keep WithName for reverse routing.
Customizing the OpenAPI document
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((doc, ctx, ct) =>
{
doc.Info.Title = "My API";
doc.Info.Version = "1.0";
doc.Info.Description = "..."
return Task.CompletedTask;
});
options.AddOperationTransformer((op, ctx, ct) =>
{
op.Responses["500"] = new OpenApiResponse { Description = "Server error" };
return Task.CompletedTask;
});
});
Transformers let you mutate the generated document/operation/schema before publishing.
Authentication in the spec
options.AddDocumentTransformer((doc, _, _) =>
{
doc.Components ??= new();
doc.Components.SecuritySchemes["bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT"
};
return Task.CompletedTask;
});
Pair endpoints with RequireAuthorization to make them require the security scheme.
Versioning + OpenAPI
builder.Services.AddApiVersioning().AddApiExplorer();
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2");
app.MapOpenApi("/openapi/{documentName}.json");
Each API version gets its own document. Combine with Asp.Versioning for Minimal APIs.
MVC controllers with OpenAPI
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(Order), 200)]
[ProducesResponseType(404)]
public async Task<IActionResult> Get(int id) { ... }
[ProducesResponseType] declares the response shape; the generator picks it up. XML comments provide summary/description (enable XML doc generation in csproj).
Swashbuckle (legacy but still common)
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
app.UseSwagger();
app.UseSwaggerUI();
Reflection-based; not AOT. Established (since 2017). Many existing projects still use it. For new .NET 9+ apps, prefer first-party Microsoft.AspNetCore.OpenApi.
Scalar / Swagger UI / ReDoc
The OpenAPI document is JSON; you choose a renderer for the interactive UI:
- Scalar — modern, beautiful, recommended in 2026.
- Swagger UI — long-standing.
- ReDoc — read-focused, good for static docs.
- Stoplight Elements — newer, good for stub/mock generation.
Generated clients
From the OpenAPI spec, generate type-safe clients:
# Microsoft kiota (recommended for .NET; produces clean client + DI)
kiota generate -d https://api.example.com/openapi.json -l csharp -o ./Clients/MyApi
# NSwag (mature; configurable)
nswag run nswag.json
In the generated client:
var client = new MyApiClient(new HttpClient { BaseAddress = new("https://api.example.com") });
var order = await client.Orders[42].GetAsync();
Tag-based grouping
var orders = app.MapGroup("/api/orders").WithTags("Orders");
var users = app.MapGroup("/api/users").WithTags("Users");
UI groups endpoints by tag. Cleaner navigation when you have many endpoints.
[Obsolete] flows through
The OpenAPI generator marks deprecated: true. Clients consuming the spec see the deprecation.
Code: correct vs wrong
❌ Wrong: untyped Results in Minimal API
app.MapGet("/orders/{id}", (int id) => Results.Ok(/* ... */));
// OpenAPI doesn't know response type
✅ Correct: TypedResults
❌ Wrong: missing [ProducesResponseType] in MVC
[HttpGet("{id}")]
public IActionResult Get(int id) { ... }
// Generator doesn't know what's returned
✅ Correct
[HttpGet("{id}")]
[ProducesResponseType(typeof(Order), 200)]
[ProducesResponseType(404)]
public ActionResult<Order> Get(int id) { ... }
❌ Wrong: hand-edited OpenAPI spec
Don't keep a hand-edited OpenAPI YAML alongside auto-generated. Generate from code; keep the code as source-of-truth.
Design patterns for this topic
Pattern 1 — "TypedResults + metadata fluent"
- Intent: accurate spec from code.
Pattern 2 — "Document transformers for cross-cutting concerns"
- Intent: add auth schemes, info metadata, custom server URLs.
Pattern 3 — "Generated clients from spec"
- Intent: type-safe consumers; keep in lockstep.
Pattern 4 — "Tag-based grouping in UI"
- Intent: clean navigation by feature.
Pattern 5 — "Versioned spec per API version"
- Intent: clients pick the version they consume.
Pros & cons / trade-offs
| Approach | Pros | Cons |
|---|---|---|
Microsoft.AspNetCore.OpenApi | First-party; AOT | .NET 9+ only |
| Swashbuckle | Mature; flexible | Reflection-based; not AOT |
| Scalar UI | Modern | Newer ecosystem |
| Swagger UI | Familiar | Heavier UI |
| kiota client | Type-safe; modern | Different style than NSwag |
When to use / when to avoid
- Use
Microsoft.AspNetCore.OpenApifor new .NET 9+ apps. - Use Scalar as the preferred UI in 2026.
- Use generated clients for cross-team/external consumption.
- Avoid hand-editing the spec alongside auto-gen.
- Avoid Swashbuckle for new projects unless you need its specific extensions.
Interview Q&A
Q1. Difference between OpenAPI and Swagger? Swagger was the original; renamed to OpenAPI in 3.0. People often use the names interchangeably. The spec is "OpenAPI Specification (OAS)"; "Swagger" is the original tool ecosystem (Swashbuckle, Swagger UI, etc.).
Q2. What's Microsoft.AspNetCore.OpenApi? First-party OpenAPI generator (.NET 9+). AOT-friendly; replaces Swashbuckle for new code.
Q3. How do you generate accurate response schemas in Minimal APIs? Use TypedResults<T1, T2, ...> with concrete types. Generator infers schema and status codes.
Q4. How do you customize the spec? Document/operation transformers via AddOpenApi(opts => opts.AddDocumentTransformer(...)).
Q5. What's Scalar? Modern OpenAPI UI alternative to Swagger UI. Cleaner design; MapScalarApiReference().
Q6. How do you generate a client from the spec? Microsoft kiota or NSwag. Run against the spec; produces typed C# client.
Q7. Versioning in OpenAPI? Per-version document: AddOpenApi("v1"), AddOpenApi("v2"). Map separately.
Q8. How do you mark an endpoint as deprecated? [Obsolete] attribute or WithDescription("...deprecated...") plus Deprecated = true in metadata.
Q9. Should you keep a hand-edited spec? No — auto-generate from code. Code is source-of-truth; spec is output.
Q10. Why is the first-party generator AOT-friendly? Designed without runtime reflection on app types. Source-gen-aware. Works under PublishAot.
Gotchas / common mistakes
- ⚠️ Untyped
IResult— vague spec. - ⚠️ Missing
[ProducesResponseType]in MVC — incomplete spec. - ⚠️ Hand-edited spec alongside auto-gen — drift.
- ⚠️ Forgetting auth scheme in document transformer — clients can't authenticate via UI.
- ⚠️ Different UI for different audiences without thought — pick one.