cit Customization
Avoid default registration. Configure document metadata, security schemes, and operation filters to enforce consistency.
// Program.cs
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Any;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Core API",
Version = "v1",
Description = "Public contract for order and inventory services",
Contact = new OpenApiContact { Name = "Platform Team", Email = "api@company.com" }
});
// Inject XML comments
var xmlPath = Path.Combine(AppContext.BaseDirectory, "MyApi.xml");
options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
// Security scheme for JWT
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "Enter 'Bearer' followed by a space and the JWT token."
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, ReferenceId = "Bearer" }
},
new string[] { }
}
});
// Filter to document standard error responses
options.OperationFilter<ErrorResponseFilter>();
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Core API v1");
options.RoutePrefix = "docs";
});
}
app.MapGet("/orders/{id}", (int id) => Results.Ok(new { Id = id, Status = "Processed" }))
.WithName("GetOrder")
.WithOpenApi(op =>
{
op.Summary = "Retrieve order details by ID";
op.Description = "Returns order payload including status and line items.";
return op;
});
app.Run();
Step 3: Implement Operation Filter for Consistent Error Responses
Automate documentation for standard HTTP error shapes instead of repeating [ProducesResponseType] across endpoints.
public class ErrorResponseFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
operation.Responses.TryAdd("400", new OpenApiResponse { Description = "Invalid request payload" });
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Missing or invalid authentication token" });
operation.Responses.TryAdd("404", new OpenApiResponse { Description = "Resource not found" });
operation.Responses.TryAdd("500", new OpenApiResponse { Description = "Internal server error" });
}
}
Step 4: Pre-Generate Spec in CI/CD
Runtime generation is acceptable for development. Production should publish a static openapi.json artifact.
# .github/workflows/api-docs.yml
name: Generate & Publish OpenAPI Spec
on:
push:
branches: [ main ]
jobs:
spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- run: dotnet restore
- run: dotnet build --no-restore -c Release
- run: dotnet tool install -g Swashbuckle.AspNetCore.Cli
- run: swagger tofile --output openapi.json --host localhost --port 5001 ./bin/Release/net8.0/MyApi.dll v1
- name: Validate OpenAPI Spec
run: |
npx @apidevtools/swagger-cli validate openapi.json
- name: Upload Spec Artifact
uses: actions/upload-artifact@v4
with:
name: openapi-spec
path: openapi.json
Architecture Decisions & Rationale
- Swashbuckle over NSwag for runtime: Swashbuckle integrates cleanly with ASP.NET Core routing, supports Minimal APIs natively, and maintains an active contributor base. NSwag is superior for client SDK generation but adds heavier runtime dependencies.
- Static spec generation in CI: Runtime generation adds memory pressure and delays first request. A pre-compiled spec ensures auditability, enables contract testing, and decouples documentation from application startup.
- OpenAPI 3.0.3 baseline: Guarantees compatibility with modern tooling (OpenAPI Generator, Stoplight, Redocly) while avoiding deprecated 2.0 patterns.
- XML comments +
[WithOpenApi] hybrid: XML comments scale for bulk documentation. Minimal API delegates override specific fields without polluting the type system.
Pitfall Guide
-
Treating auto-generated docs as final
Auto-generation captures signatures, not intent. Missing descriptions, ambiguous parameter names, and undocumented edge cases remain. Always pair generation with explicit [OpenApi] overrides or XML comments that explain business constraints.
-
Omitting security scheme definitions
Swagger UI will display endpoints but fail to attach tokens to requests. Define AddSecurityDefinition and AddSecurityRequirement explicitly. Test the UI's lock icon behavior before publishing.
-
Hardcoding server URLs in production specs
Static specs should not contain environment-specific hosts. Use relative paths or inject servers dynamically via middleware or API management gateways. Hardcoded URLs break cross-environment consumption.
-
Leaking internal types in the spec
Exposing implementation DTOs, EF Core entities, or internal error wrappers violates contract boundaries. Map to explicit API response models and exclude internal namespaces via options.DocInclusionPredicate.
-
Skipping OpenAPI schema validation
Generated specs frequently contain invalid references, missing responses blocks, or malformed security requirements. Validate every artifact against the OpenAPI 3.0 schema in CI. Unvalidated specs break client generators and API gateways.
-
Ignoring versioning in documentation
Multiple API versions in a single Swagger UI cause route collisions and client confusion. Use options.DocInclusionPredicate to isolate versions, publish separate spec files, and route consumers via explicit version prefixes.
-
Not generating client SDKs alongside specs
Documentation without consumable contracts remains theoretical. Integrate openapi-generator-cli or NSwagStudio in the pipeline to produce typed clients for TypeScript, Java, and C#. Ship SDKs as versioned packages.
Best Practices from Production:
- Enforce
[ProducesResponseType] for non-200 responses on critical endpoints.
- Use
IEndpointConventionBuilder extensions to standardize documentation patterns across teams.
- Run contract tests (Pact or WireMock) against the generated spec before deployment.
- Rotate spec artifacts with semantic versioning; never overwrite published contracts.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal microservice with frequent schema changes | Runtime Swashbuckle + dev-only UI | Fast iteration, no pipeline overhead required | Low infrastructure cost, moderate client risk |
| Public-facing API with external consumers | CI-generated static spec + SDK publishing | Contract stability, auditability, client readiness | Higher pipeline setup cost, lower support overhead |
| Multi-version API (v1, v2, v3) | Isolated doc inclusion predicates + versioned artifacts | Prevents route collisions, enables gradual migration | Moderate configuration effort, high consumer satisfaction |
| Legacy .NET Framework API | NSwagStudio + manual OpenAPI export | Bridges older routing model, produces reliable client code | Higher maintenance overhead, requires manual sync |
Configuration Template
// SwaggerSetup.cs
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Any;
public static class SwaggerSetup
{
public static void AddApiDocumentation(this IServiceCollection services, IConfiguration config)
{
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = config["Api:Title"] ?? "API Contract",
Version = "v1",
Description = config["Api:Description"] ?? "Public API specification"
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Bearer token"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, ReferenceId = "Bearer" }
},
Array.Empty<string>()
}
});
options.OperationFilter<StandardErrorResponseFilter>();
options.DocInclusionPredicate((_, api) => !api.RelativePath.Contains("internal"));
});
}
}
// appsettings.json
{
"Api": {
"Title": "Order Processing API",
"Description": "Handles order creation, status tracking, and fulfillment routing."
},
"Swagger": {
"RoutePrefix": "docs",
"EnableDevOnly": true
}
}
Quick Start Guide
- Add packages: Run
dotnet add package Swashbuckle.AspNetCore and enable <GenerateDocumentationFile>true</GenerateDocumentationFile> in the .csproj.
- Register services: Call
builder.Services.AddApiDocumentation(builder.Configuration) and builder.Services.AddEndpointsApiExplorer() in Program.cs.
- Configure middleware: Wrap
app.UseSwagger() and app.UseSwaggerUI() inside an if (app.Environment.IsDevelopment()) block. Set options.RoutePrefix = "docs".
- Annotate endpoints: Add XML comments to methods or use
.WithOpenApi(op => { op.Summary = "..."; return op; }) for Minimal APIs.
- Validate locally: Navigate to
/docs, verify security lock icon behavior, and run dotnet swagger tofile --output swagger.json ./bin/Debug/net8.0/MyApi.dll v1 to confirm static export works.