624 lines
13 KiB
Markdown
624 lines
13 KiB
Markdown
# Swagger Integration
|
|
|
|
OpenAPI/Swagger documentation for HTTP endpoints.
|
|
|
|
## Overview
|
|
|
|
Svrnty.CQRS automatically integrates with Swagger/OpenAPI to provide interactive API documentation for all commands and queries.
|
|
|
|
**Benefits:**
|
|
- ✅ **Automatic documentation** - No manual OpenAPI spec writing
|
|
- ✅ **Interactive testing** - Test endpoints directly from browser
|
|
- ✅ **Schema generation** - Request/response types documented
|
|
- ✅ **Validation docs** - FluentValidation rules reflected
|
|
- ✅ **Authorization indication** - Security requirements shown
|
|
|
|
## Basic Setup
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
dotnet add package Swashbuckle.AspNetCore
|
|
```
|
|
|
|
### Configuration
|
|
|
|
```csharp
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Add Swagger services
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen();
|
|
|
|
// Register CQRS
|
|
builder.Services.AddSvrntyCQRS();
|
|
builder.Services.AddDefaultCommandDiscovery();
|
|
builder.Services.AddDefaultQueryDiscovery();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Enable Swagger middleware
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
}
|
|
|
|
// Map CQRS endpoints
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
|
|
app.Run();
|
|
```
|
|
|
|
**Access Swagger UI:**
|
|
```
|
|
https://localhost:5001/swagger
|
|
```
|
|
|
|
## Automatic Features
|
|
|
|
### Endpoint Discovery
|
|
|
|
All command and query endpoints appear automatically:
|
|
|
|
```
|
|
Commands
|
|
POST /api/command/createUser
|
|
POST /api/command/updateUser
|
|
POST /api/command/deleteUser
|
|
|
|
Queries
|
|
GET /api/query/getUser
|
|
POST /api/query/getUser
|
|
GET /api/query/listUsers
|
|
POST /api/query/listUsers
|
|
```
|
|
|
|
### Request Schemas
|
|
|
|
```csharp
|
|
public record CreateUserCommand
|
|
{
|
|
public string Name { get; init; } = string.Empty;
|
|
public string Email { get; init; } = string.Empty;
|
|
public int Age { get; init; }
|
|
}
|
|
```
|
|
|
|
**Generated Schema:**
|
|
```json
|
|
{
|
|
"CreateUserCommand": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"email": { "type": "string" },
|
|
"age": { "type": "integer", "format": "int32" }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Response Schemas
|
|
|
|
```csharp
|
|
public record UserDto
|
|
{
|
|
public int Id { get; init; }
|
|
public string Name { get; init; } = string.Empty;
|
|
public string Email { get; init; } = string.Empty;
|
|
}
|
|
```
|
|
|
|
**Generated Schema:**
|
|
```json
|
|
{
|
|
"UserDto": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": { "type": "integer", "format": "int32" },
|
|
"name": { "type": "string" },
|
|
"email": { "type": "string" }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Custom Configuration
|
|
|
|
### API Information
|
|
|
|
```csharp
|
|
builder.Services.AddSwaggerGen(options =>
|
|
{
|
|
options.SwaggerDoc("v1", new OpenApiInfo
|
|
{
|
|
Title = "My CQRS API",
|
|
Version = "v1",
|
|
Description = "API using Svrnty.CQRS framework",
|
|
Contact = new OpenApiContact
|
|
{
|
|
Name = "Your Name",
|
|
Email = "your.email@example.com",
|
|
Url = new Uri("https://example.com")
|
|
},
|
|
License = new OpenApiLicense
|
|
{
|
|
Name = "MIT",
|
|
Url = new Uri("https://opensource.org/licenses/MIT")
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
### XML Comments
|
|
|
|
Enable XML documentation comments:
|
|
|
|
**Project file (.csproj):**
|
|
```xml
|
|
<PropertyGroup>
|
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
</PropertyGroup>
|
|
```
|
|
|
|
**Code with XML comments:**
|
|
```csharp
|
|
/// <summary>
|
|
/// Creates a new user account
|
|
/// </summary>
|
|
public record CreateUserCommand
|
|
{
|
|
/// <summary>
|
|
/// User's full name
|
|
/// </summary>
|
|
/// <example>John Doe</example>
|
|
public string Name { get; init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// User's email address
|
|
/// </summary>
|
|
/// <example>john@example.com</example>
|
|
public string Email { get; init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// User's age in years
|
|
/// </summary>
|
|
/// <example>25</example>
|
|
public int Age { get; init; }
|
|
}
|
|
```
|
|
|
|
**Swagger configuration:**
|
|
```csharp
|
|
builder.Services.AddSwaggerGen(options =>
|
|
{
|
|
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
|
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
|
options.IncludeXmlComments(xmlPath);
|
|
});
|
|
```
|
|
|
|
### Example Values
|
|
|
|
```csharp
|
|
using System.ComponentModel.DataAnnotations;
|
|
|
|
public record CreateUserCommand
|
|
{
|
|
[Required]
|
|
[StringLength(100)]
|
|
public string Name { get; init; } = "John Doe";
|
|
|
|
[Required]
|
|
[EmailAddress]
|
|
public string Email { get; init; } = "john@example.com";
|
|
|
|
[Range(18, 150)]
|
|
public int Age { get; init; } = 25;
|
|
}
|
|
```
|
|
|
|
## Authentication Documentation
|
|
|
|
### JWT Bearer
|
|
|
|
```csharp
|
|
using Microsoft.OpenApi.Models;
|
|
|
|
builder.Services.AddSwaggerGen(options =>
|
|
{
|
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
|
{
|
|
Name = "Authorization",
|
|
Type = SecuritySchemeType.Http,
|
|
Scheme = "bearer",
|
|
BearerFormat = "JWT",
|
|
In = ParameterLocation.Header,
|
|
Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\""
|
|
});
|
|
|
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
|
{
|
|
{
|
|
new OpenApiSecurityScheme
|
|
{
|
|
Reference = new OpenApiReference
|
|
{
|
|
Type = ReferenceType.SecurityScheme,
|
|
Id = "Bearer"
|
|
}
|
|
},
|
|
Array.Empty<string>()
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
**Swagger UI shows "Authorize" button:**
|
|
```
|
|
Click "Authorize" → Enter: Bearer eyJhbGc...
|
|
```
|
|
|
|
### API Key
|
|
|
|
```csharp
|
|
builder.Services.AddSwaggerGen(options =>
|
|
{
|
|
options.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
|
|
{
|
|
Name = "X-API-Key",
|
|
Type = SecuritySchemeType.ApiKey,
|
|
In = ParameterLocation.Header,
|
|
Description = "API Key authentication"
|
|
});
|
|
|
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
|
{
|
|
{
|
|
new OpenApiSecurityScheme
|
|
{
|
|
Reference = new OpenApiReference
|
|
{
|
|
Type = ReferenceType.SecurityScheme,
|
|
Id = "ApiKey"
|
|
}
|
|
},
|
|
Array.Empty<string>()
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
## Grouping and Organization
|
|
|
|
### Tag Filtering
|
|
|
|
Commands and queries are automatically tagged:
|
|
|
|
```json
|
|
{
|
|
"paths": {
|
|
"/api/command/createUser": {
|
|
"post": {
|
|
"tags": ["Commands"]
|
|
}
|
|
},
|
|
"/api/query/getUser": {
|
|
"get": {
|
|
"tags": ["Queries"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Custom Tags
|
|
|
|
Add custom tag descriptions:
|
|
|
|
```csharp
|
|
builder.Services.AddSwaggerGen(options =>
|
|
{
|
|
options.SwaggerDoc("v1", new OpenApiInfo
|
|
{
|
|
Title = "My CQRS API",
|
|
Version = "v1"
|
|
});
|
|
|
|
// Tag descriptions
|
|
options.TagActionsBy(api =>
|
|
{
|
|
if (api.GroupName != null)
|
|
{
|
|
return new[] { api.GroupName };
|
|
}
|
|
|
|
if (api.ActionDescriptor is EndpointMetadataApiDescriptionProvider.EndpointMetadataApiDescription emad)
|
|
{
|
|
if (emad.EndpointMetadata.OfType<TagsAttribute>().FirstOrDefault() is { } tagsAttribute)
|
|
{
|
|
return tagsAttribute.Tags;
|
|
}
|
|
}
|
|
|
|
return new[] { "Other" };
|
|
});
|
|
|
|
options.DocInclusionPredicate((name, api) => true);
|
|
});
|
|
```
|
|
|
|
## Response Documentation
|
|
|
|
### Success Responses
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// Creates a new user
|
|
/// </summary>
|
|
/// <response code="200">User created successfully</response>
|
|
/// <response code="400">Validation failed</response>
|
|
/// <response code="401">Not authenticated</response>
|
|
/// <response code="403">Not authorized</response>
|
|
public record CreateUserCommand { }
|
|
```
|
|
|
|
### Validation Error Example
|
|
|
|
Swagger automatically documents validation errors:
|
|
|
|
```json
|
|
{
|
|
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
|
"title": "One or more validation errors occurred.",
|
|
"status": 400,
|
|
"errors": {
|
|
"Name": ["Name is required"],
|
|
"Email": ["Valid email address is required"]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Versioning
|
|
|
|
### Multiple API Versions
|
|
|
|
```csharp
|
|
builder.Services.AddSwaggerGen(options =>
|
|
{
|
|
options.SwaggerDoc("v1", new OpenApiInfo
|
|
{
|
|
Title = "My CQRS API",
|
|
Version = "v1"
|
|
});
|
|
|
|
options.SwaggerDoc("v2", new OpenApiInfo
|
|
{
|
|
Title = "My CQRS API",
|
|
Version = "v2"
|
|
});
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseSwaggerUI(options =>
|
|
{
|
|
options.SwaggerEndpoint("/swagger/v1/swagger.json", "V1");
|
|
options.SwaggerEndpoint("/swagger/v2/swagger.json", "V2");
|
|
});
|
|
```
|
|
|
|
### Version-Specific Endpoints
|
|
|
|
```csharp
|
|
app.MapSvrntyCommands("v1/commands");
|
|
app.MapSvrntyCommands("v2/commands");
|
|
```
|
|
|
|
## Customization
|
|
|
|
### UI Customization
|
|
|
|
```csharp
|
|
app.UseSwaggerUI(options =>
|
|
{
|
|
options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
|
|
options.RoutePrefix = "api-docs"; // Change from /swagger to /api-docs
|
|
options.DocumentTitle = "My CQRS API Documentation";
|
|
options.DisplayRequestDuration();
|
|
options.EnableDeepLinking();
|
|
options.EnableFilter();
|
|
options.ShowExtensions();
|
|
options.EnableValidator();
|
|
});
|
|
```
|
|
|
|
### Custom CSS
|
|
|
|
```csharp
|
|
app.UseSwaggerUI(options =>
|
|
{
|
|
options.InjectStylesheet("/swagger-ui/custom.css");
|
|
});
|
|
|
|
// Serve custom CSS
|
|
app.UseStaticFiles();
|
|
```
|
|
|
|
**wwwroot/swagger-ui/custom.css:**
|
|
```css
|
|
.swagger-ui .topbar {
|
|
background-color: #2c3e50;
|
|
}
|
|
```
|
|
|
|
## Production Deployment
|
|
|
|
### Disable Swagger in Production
|
|
|
|
```csharp
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
}
|
|
```
|
|
|
|
### Enable with Authentication
|
|
|
|
```csharp
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI(options =>
|
|
{
|
|
options.ConfigObject.AdditionalItems["onComplete"] = new Action(() =>
|
|
{
|
|
// Require login before showing Swagger
|
|
});
|
|
});
|
|
|
|
app.MapSwagger().RequireAuthorization();
|
|
```
|
|
|
|
## Testing with Swagger
|
|
|
|
### Try It Out
|
|
|
|
1. Navigate to Swagger UI: `https://localhost:5001/swagger`
|
|
2. Expand endpoint (e.g., `POST /api/command/createUser`)
|
|
3. Click "Try it out"
|
|
4. Fill in request body:
|
|
```json
|
|
{
|
|
"name": "John Doe",
|
|
"email": "john@example.com",
|
|
"age": 25
|
|
}
|
|
```
|
|
5. Click "Execute"
|
|
6. View response
|
|
|
|
### Authentication
|
|
|
|
1. Click "Authorize" button
|
|
2. Enter Bearer token: `Bearer eyJhbGc...`
|
|
3. Click "Authorize"
|
|
4. Close dialog
|
|
5. All requests now include authentication
|
|
|
|
## Complete Example
|
|
|
|
```csharp
|
|
using Microsoft.OpenApi.Models;
|
|
using System.Reflection;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// CQRS
|
|
builder.Services.AddSvrntyCQRS();
|
|
builder.Services.AddDefaultCommandDiscovery();
|
|
builder.Services.AddDefaultQueryDiscovery();
|
|
|
|
// Swagger
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen(options =>
|
|
{
|
|
options.SwaggerDoc("v1", new OpenApiInfo
|
|
{
|
|
Title = "My CQRS API",
|
|
Version = "v1",
|
|
Description = "An API using Svrnty.CQRS framework",
|
|
Contact = new OpenApiContact
|
|
{
|
|
Name = "Your Team",
|
|
Email = "team@example.com"
|
|
}
|
|
});
|
|
|
|
// XML comments
|
|
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
|
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
|
if (File.Exists(xmlPath))
|
|
{
|
|
options.IncludeXmlComments(xmlPath);
|
|
}
|
|
|
|
// JWT Authentication
|
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
|
{
|
|
Name = "Authorization",
|
|
Type = SecuritySchemeType.Http,
|
|
Scheme = "bearer",
|
|
BearerFormat = "JWT",
|
|
In = ParameterLocation.Header,
|
|
Description = "Enter JWT token"
|
|
});
|
|
|
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
|
{
|
|
{
|
|
new OpenApiSecurityScheme
|
|
{
|
|
Reference = new OpenApiReference
|
|
{
|
|
Type = ReferenceType.SecurityScheme,
|
|
Id = "Bearer"
|
|
}
|
|
},
|
|
Array.Empty<string>()
|
|
}
|
|
});
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
// Enable Swagger in development
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI(options =>
|
|
{
|
|
options.SwaggerEndpoint("/swagger/v1/swagger.json", "V1");
|
|
options.DisplayRequestDuration();
|
|
options.EnableFilter();
|
|
});
|
|
}
|
|
|
|
// CQRS endpoints
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
|
|
app.Run();
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### ✅ DO
|
|
|
|
- Add XML documentation comments
|
|
- Configure authentication schemes
|
|
- Use example values
|
|
- Version your API
|
|
- Organize endpoints with tags
|
|
- Test endpoints via Swagger UI
|
|
- Disable Swagger in production (or secure it)
|
|
- Document response codes
|
|
|
|
### ❌ DON'T
|
|
|
|
- Don't expose Swagger publicly without authentication
|
|
- Don't skip XML comments
|
|
- Don't ignore API versioning
|
|
- Don't leave default titles/descriptions
|
|
- Don't expose internal endpoints
|
|
|
|
## See Also
|
|
|
|
- [HTTP Integration Overview](README.md)
|
|
- [Endpoint Mapping](endpoint-mapping.md)
|
|
- [HTTP Configuration](http-configuration.md)
|
|
- [HTTP Troubleshooting](http-troubleshooting.md)
|
|
- [Swashbuckle Documentation](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)
|