dotnet-cqrs/docs/http-integration/swagger-integration.md

13 KiB

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

dotnet add package Swashbuckle.AspNetCore

Configuration

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

public record CreateUserCommand
{
    public string Name { get; init; } = string.Empty;
    public string Email { get; init; } = string.Empty;
    public int Age { get; init; }
}

Generated Schema:

{
  "CreateUserCommand": {
    "type": "object",
    "properties": {
      "name": { "type": "string" },
      "email": { "type": "string" },
      "age": { "type": "integer", "format": "int32" }
    }
  }
}

Response Schemas

public record UserDto
{
    public int Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public string Email { get; init; } = string.Empty;
}

Generated Schema:

{
  "UserDto": {
    "type": "object",
    "properties": {
      "id": { "type": "integer", "format": "int32" },
      "name": { "type": "string" },
      "email": { "type": "string" }
    }
  }
}

Custom Configuration

API Information

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):

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

Code with XML comments:

/// <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:

builder.Services.AddSwaggerGen(options =>
{
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    options.IncludeXmlComments(xmlPath);
});

Example Values

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

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

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:

{
  "paths": {
    "/api/command/createUser": {
      "post": {
        "tags": ["Commands"]
      }
    },
    "/api/query/getUser": {
      "get": {
        "tags": ["Queries"]
      }
    }
  }
}

Custom Tags

Add custom tag descriptions:

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

/// <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:

{
  "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

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

app.MapSvrntyCommands("v1/commands");
app.MapSvrntyCommands("v2/commands");

Customization

UI Customization

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

app.UseSwaggerUI(options =>
{
    options.InjectStylesheet("/swagger-ui/custom.css");
});

// Serve custom CSS
app.UseStaticFiles();

wwwroot/swagger-ui/custom.css:

.swagger-ui .topbar {
    background-color: #2c3e50;
}

Production Deployment

Disable Swagger in Production

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

Enable with Authentication

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:
    {
      "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

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