Previous Article: ASP.NET Core Advanced Authorization: Policy-Based Security & Resource Protection Guide

Table of Contents
Introduction to Swagger/OpenAPI
Setting Up Swagger in ASP.NET Core
Basic Configuration and Customization
API Documentation with XML Comments
Advanced Swagger Configuration
Security and Authentication
Versioning with Swagger
Real-World E-Commerce API Example
Testing APIs with Swagger UI
Best Practices and Common Pitfalls
Alternatives to Swashbuckle
Conclusion
1. Introduction to Swagger/OpenAPI
What is Swagger and OpenAPI?
Swagger is a powerful, open-source framework that helps developers design, build, document, and consume RESTful web services. The OpenAPI Specification (formerly Swagger Specification) is a standard, language-agnostic interface description for REST APIs that allows both humans and computers to discover and understand the capabilities of a service without access to source code or documentation.
Why Swagger is Essential for Modern API Development
In today's microservices architecture, APIs are the backbone of applications. Swagger addresses critical challenges:
Automatic Documentation: Generates interactive API documentation automatically
Client SDK Generation: Creates client libraries in multiple languages
API Testing: Provides built-in testing interface
Standardization: Ensures consistent API design across teams
Discovery: Makes APIs self-describing and discoverable
Real-Life Scenario: The API Documentation Crisis
Imagine you're joining a new project with 50+ REST endpoints. Without proper documentation, you'd spend days:
Reading through controller code
Testing endpoints manually with Postman
Asking team members about request/response formats
Guessing authentication requirements
With Swagger, you get instant, interactive documentation that's always in sync with your code.
csharp
// Before Swagger - Traditional API Documentation/*
API: /api/products
Method: GET
Description: Get all products
Parameters: page (optional), pageSize (optional)
Response: List of Product objects
Authentication: Bearer token required
*/
// After Swagger - Automatic, Interactive Documentation// The same information is automatically generated and always up-to-date2. Setting Up Swagger in ASP.NET Core
Prerequisites and Installation
Let's start by creating a new ASP.NET Core Web API project and adding Swagger support.
# Create new ASP.NET Core Web API project
dotnet new webapi -n ECommerceAPI
cd ECommerceAPI
# Add Swashbuckle.AspNetCore package
dotnet add package Swashbuckle.AspNetCoreBasic Setup in Program.cs
Here's the minimal setup to get Swagger running:
// Program.csusing Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
// Configure Swagger
builder.Services.AddSwaggerGen(c =>{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "ECommerce API",
Version = "v1",
Description = "A complete e-commerce API for managing products, orders, and customers",
Contact = new OpenApiContact
{
Name = "API Support",
Email = "[email protected]"
}
});});
var app = builder.Build();
// Configure the HTTP request pipelineif (app.Environment.IsDevelopment()){
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "ECommerce API v1");
});}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();Testing Your Setup
Run the application and navigate to:
Swagger JSON:
https://localhost:7000/swagger/v1/swagger.jsonSwagger UI:
https://localhost:7000/swagger
You should see the interactive Swagger UI with your API endpoints.
3. Basic Configuration and Customization
Customizing Swagger UI Appearance
Make your API documentation stand out with custom branding:
// Program.cs - Enhanced Swagger Configuration
builder.Services.AddSwaggerGen(c =>{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "ECommerce API",
Version = "v1",
Description = "A complete e-commerce API",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "Development Team",
Email = "[email protected]",
Url = new Uri("https://twitter.com/ecommerceapi")
},
License = new OpenApiLicense
{
Name = "ECommerce API License",
Url = new Uri("https://example.com/license")
}
});
// Set the comments path for the Swagger JSON and UI
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);});
// In Configure method
app.UseSwaggerUI(c =>{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "ECommerce API v1");
// Customizations
c.DocumentTitle = "ECommerce API Documentation";
c.RoutePrefix = "api-docs"; // Change the URL path
c.InjectStylesheet("/swagger-ui/custom.css"); // Custom CSS
c.EnableValidator();
c.EnableDeepLinking();
c.DisplayOperationId();
c.DisplayRequestDuration();
c.DefaultModelsExpandDepth(2);
c.DefaultModelExpandDepth(2);});Adding Custom CSS for Branding
Create wwwroot/swagger-ui/custom.css:
/* wwwroot/swagger-ui/custom.css */.swagger-ui .topbar {
background-color: #2c3e50;
padding: 10px 0;}
.swagger-ui .topbar .download-url-wrapper {
display: none;}
.swagger-ui .info hgroup.main {
text-align: center;}
.swagger-ui .info .title {
color: #3b4151;
font-family: sans-serif;
font-size: 36px;}
.swagger-ui .btn.authorize {
background-color: #3498db;
border-color: #3498db;}
.swagger-ui .btn.authorize svg {
fill: white;}4. API Documentation with XML Comments
Enabling XML Documentation
Add to your .csproj file:
<PropertyGroup><GenerateDocumentationFile>true</GenerateDocumentationFile><NoWarn>$(NoWarn);1591</NoWarn></PropertyGroup>Comprehensive Controller Documentation Example
using Microsoft.AspNetCore.Mvc;using System.ComponentModel.DataAnnotations;
namespace ECommerceAPI.Controllers{
/// <summary>
/// Provides operations for managing products in the e-commerce system
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
private static readonly List<Product> _products = new()
{
new Product { Id = 1, Name = "Laptop", Price = 999.99m, Category = "Electronics", Stock = 10 },
new Product { Id = 2, Name = "Book", Price = 19.99m, Category = "Education", Stock = 100 }
};
/// <summary>
/// Retrieves all products with optional filtering and pagination
/// </summary>
/// <param name="category">Filter products by category</param>
/// <param name="page">Page number for pagination (default: 1)</param>
/// <param name="pageSize">Number of items per page (default: 10, max: 50)</param>
/// <returns>List of products matching the criteria</returns>
/// <response code="200">Returns the list of products</response>
/// <response code="400">If the request parameters are invalid</response>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<ApiResponse<List<Product>>> GetProducts(
[FromQuery] string? category = null,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
if (page < 1 || pageSize < 1 || pageSize > 50)
{
return BadRequest(new ApiResponse<object>
{
Success = false,
Message = "Invalid pagination parameters",
Data = null
});
}
var filteredProducts = _products
.Where(p => category == null || p.Category.Equals(category, StringComparison.OrdinalIgnoreCase))
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
return Ok(new ApiResponse<List<Product>>
{
Success = true,
Message = "Products retrieved successfully",
Data = filteredProducts
});
}
/// <summary>
/// Retrieves a specific product by its unique identifier
/// </summary>
/// <param name="id">The product ID</param>
/// <returns>The requested product</returns>
/// <response code="200">Returns the requested product</response>
/// <response code="404">If the product is not found</response>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ApiResponse<Product>> GetProduct(int id)
{
var product = _products.FirstOrDefault(p => p.Id == id);
if (product == null)
{
return NotFound(new ApiResponse<object>
{
Success = false,
Message = $"Product with ID {id} not found",
Data = null
});
}
return Ok(new ApiResponse<Product>
{
Success = true,
Message = "Product retrieved successfully",
Data = product
});
}
/// <summary>
/// Creates a new product in the system
/// </summary>
/// <param name="product">The product data</param>
/// <returns>The created product with generated ID</returns>
/// <response code="201">Returns the newly created product</response>
/// <response code="400">If the product data is invalid</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<ApiResponse<Product>> CreateProduct([FromBody] CreateProductRequest product)
{
if (!ModelState.IsValid)
{
return BadRequest(new ApiResponse<object>
{
Success = false,
Message = "Invalid product data",
Data = ModelState.Values.SelectMany(v => v.Errors)
});
}
var newProduct = new Product
{
Id = _products.Max(p => p.Id) + 1,
Name = product.Name,
Price = product.Price,
Category = product.Category,
Stock = product.Stock
};
_products.Add(newProduct);
return CreatedAtAction(nameof(GetProduct), new { id = newProduct.Id }, new ApiResponse<Product>
{
Success = true,
Message = "Product created successfully",
Data = newProduct
});
}
}
/// <summary>
/// Represents a product in the e-commerce system
/// </summary>
public class Product
{
/// <summary>
/// The unique identifier for the product
/// </summary>
/// <example>1</example>
public int Id { get; set; }
/// <summary>
/// The name of the product
/// </summary>
/// <example>Wireless Mouse</example>
[Required]
public string Name { get; set; } = string.Empty;
/// <summary>
/// The price of the product in USD
/// </summary>
/// <example>29.99</example>
[Range(0.01, double.MaxValue)]
public decimal Price { get; set; }
/// <summary>
/// The category of the product
/// </summary>
/// <example>Electronics</example>
[Required]
public string Category { get; set; } = string.Empty;
/// <summary>
/// The number of items available in stock
/// </summary>
/// <example>50</example>
[Range(0, int.MaxValue)]
public int Stock { get; set; }
}
/// <summary>
/// Request model for creating a new product
/// </summary>
public class CreateProductRequest
{
/// <summary>
/// The name of the product (2-100 characters)
/// </summary>
/// <example>Mechanical Keyboard</example>
[Required]
[StringLength(100, MinimumLength = 2)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// The price of the product (must be greater than 0)
/// </summary>
/// <example>79.99</example>
[Range(0.01, double.MaxValue)]
public decimal Price { get; set; }
/// <summary>
/// The category of the product
/// </summary>
/// <example>Electronics</example>
[Required]
public string Category { get; set; } = string.Empty;
/// <summary>
/// The initial stock quantity
/// </summary>
/// <example>25</example>
[Range(0, int.MaxValue)]
public int Stock { get; set; }
}
/// <summary>
/// Standard API response format
/// </summary>
/// <typeparam name="T">The type of data being returned</typeparam>
public class ApiResponse<T>
{
/// <summary>
/// Indicates whether the request was successful
/// </summary>
/// <example>true</example>
public bool Success { get; set; }
/// <summary>
/// A message describing the result of the operation
/// </summary>
/// <example>Operation completed successfully</example>
public string Message { get; set; } = string.Empty;
/// <summary>
/// The actual data payload
/// </summary>
public T? Data { get; set; }
}}5. Advanced Swagger Configuration
Operation Filters for Custom Behavior
Create custom operation filters to enhance Swagger documentation:
// CustomOperationFilter.csusing Microsoft.OpenApi.Models;using Swashbuckle.AspNetCore.SwaggerGen;using System.Reflection;
public class CustomOperationFilter : IOperationFilter{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Add custom header parameter to all operations
operation.Parameters ??= new List<OpenApiParameter>();
operation.Parameters.Add(new OpenApiParameter
{
Name = "X-Correlation-Id",
In = ParameterLocation.Header,
Required = false,
Description = "Correlation ID for request tracking",
Schema = new OpenApiSchema { Type = "string" }
});
// Add custom response examples based on method name
var methodName = context.MethodInfo.Name.ToLower();
if (methodName.Contains("get") && context.MethodInfo.ReturnType.GenericTypeArguments.Any())
{
operation.Responses["200"].Description = "Successfully retrieved data";
}
// Mark deprecated methods
if (context.MethodInfo.GetCustomAttribute<ObsoleteAttribute>() != null)
{
operation.Deprecated = true;
}
}}
// Register the filter
builder.Services.AddSwaggerGen(c =>{
c.OperationFilter<CustomOperationFilter>();});Schema Filters for Custom Model Documentation
// CustomSchemaFilter.csusing Microsoft.OpenApi.Models;using Swashbuckle.AspNetCore.SwaggerGen;
public class CustomSchemaFilter : ISchemaFilter{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type == typeof(Product))
{
schema.Description = "Represents a product in the e-commerce catalog with inventory tracking";
// Add example to schema
schema.Example = new Microsoft.OpenApi.Any.OpenApiObject
{
["id"] = new Microsoft.OpenApi.Any.OpenApiInteger(1),
["name"] = new Microsoft.OpenApi.Any.OpenApiString("Wireless Mouse"),
["price"] = new Microsoft.OpenApi.Any.OpenApiDouble(29.99),
["category"] = new Microsoft.OpenApi.Any.OpenApiString("Electronics"),
["stock"] = new Microsoft.OpenApi.Any.OpenApiInteger(50)
};
}
// Add format information for decimal properties
if (context.Type == typeof(decimal) || context.Type == typeof(decimal?))
{
schema.Format = "decimal";
schema.Description = "Monetary value in USD";
}
}}
// Register schema filter
builder.Services.AddSwaggerGen(c =>{
c.SchemaFilter<CustomSchemaFilter>();});Document Filters for Global Modifications
// CustomDocumentFilter.csusing Microsoft.OpenApi.Models;using Swashbuckle.AspNetCore.SwaggerGen;
public class CustomDocumentFilter : IDocumentFilter{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
// Add global tags
swaggerDoc.Tags = new List<OpenApiTag>
{
new OpenApiTag { Name = "Products", Description = "Operations related to product management" },
new OpenApiTag { Name = "Orders", Description = "Operations related to order processing" },
new OpenApiTag { Name = "Customers", Description = "Customer management operations" }
};
// Add server information
swaggerDoc.Servers = new List<OpenApiServer>
{
new OpenApiServer { Url = "https://api.ecommerce.com/v1", Description = "Production Server" },
new OpenApiServer { Url = "https://staging-api.ecommerce.com/v1", Description = "Staging Server" },
new OpenApiServer { Url = "https://localhost:7000", Description = "Development Server" }
};
// Add custom info
swaggerDoc.Info.Extensions.Add("x-api-version", new Microsoft.OpenApi.Any.OpenApiString("1.0.0"));
}}
// Register document filter
builder.Services.AddSwaggerGen(c =>{
c.DocumentFilter<CustomDocumentFilter>();});6. Security and Authentication
JWT Bearer Token Configuration
Configure Swagger to handle JWT authentication:
// Program.cs - JWT Configuration
builder.Services.AddSwaggerGen(c =>{
// ... existing configuration ...
// Add JWT Authentication
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = @"JWT Authorization header using the Bearer scheme.
Enter 'Bearer' [space] and then your token in the text input below.
Example: 'Bearer 12345abcdef'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});});
// Configure JWT Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
Join the conversation! Your thoughts help the community grow.