Previous Article: ASP.NET Core Advanced Authorization: Policy-Based Security & Resource Protection Guide
![ASP.NET Core Swagger Mastery: Interactive API Documentation Guide (Part-15 of 40) ASP.NET Core Swagger Mastery: Interactive API Documentation Guide (Part-15 of 40)]()
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-date
2. 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.AspNetCore
Basic 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:
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"]))
};
});
API Key Authentication
For simpler APIs, you might use API key authentication:
// Program.cs - API Key Configuration
builder.Services.AddSwaggerGen(c =>{
c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
{
Description = "API Key authentication",
Name = "X-API-Key",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "ApiKeyScheme"
});
var scheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "ApiKey"
},
In = ParameterLocation.Header
};
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{ scheme, new List<string>() }
});});
OAuth2 Configuration
For more complex authentication scenarios:
// Program.cs - OAuth2 Configuration
builder.Services.AddSwaggerGen(c =>{
c.AddSecurityDefinition("OAuth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri("https://example.com/oauth/authorize"),
TokenUrl = new Uri("https://example.com/oauth/token"),
Scopes = new Dictionary<string, string>
{
{ "read", "Read access" },
{ "write", "Write access" }
}
}
}
});});
7. Versioning with Swagger
Setting Up API Versioning
Install the versioning packages:
dotnet add package Microsoft.AspNetCore.Mvc.Versioning
dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
Configure API Versioning
// Program.cs - Versioning Configuration
builder.Services.AddApiVersioning(options =>{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("x-api-version"),
new MediaTypeApiVersionReader("version")
);});
builder.Services.AddVersionedApiExplorer(options =>{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;});
Multi-Version Swagger Configuration
// Program.cs - Multi-version Swagger
builder.Services.AddSwaggerGen(c =>{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "ECommerce API v1",
Version = "v1",
Description = "Initial version of the ECommerce API"
});
c.SwaggerDoc("v2", new OpenApiInfo
{
Title = "ECommerce API v2",
Version = "v2",
Description = "Enhanced version with new features"
});
// Resolve conflicts when same route exists in multiple versions
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());});
// In Configure methodvar apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
app.UseSwaggerUI(options =>{
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}});
Versioned Controller Example
// Controllers/ProductsV2Controller.csnamespace ECommerceAPI.Controllers{
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
/// <summary>
/// Retrieves products with advanced filtering and sorting (v2)
/// </summary>
[HttpGet]
[MapToApiVersion("2.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<ApiResponse<PagedResult<Product>>> GetProductsV2(
[FromQuery] ProductQueryParameters queryParams)
{
// V2 implementation with advanced features
return Ok(new ApiResponse<PagedResult<Product>>
{
Success = true,
Message = "Products retrieved successfully (v2)",
Data = new PagedResult<Product>
{
Items = new List<Product>(),
TotalCount = 0,
Page = queryParams.Page,
PageSize = queryParams.PageSize
}
});
}
}
/// <summary>
/// Advanced product query parameters for v2
/// </summary>
public class ProductQueryParameters
{
/// <summary>
/// Search term for product name or description
/// </summary>
public string? Search { get; set; }
/// <summary>
/// Filter by category
/// </summary>
public string? Category { get; set; }
/// <summary>
/// Minimum price filter
/// </summary>
[Range(0, double.MaxValue)]
public decimal? MinPrice { get; set; }
/// <summary>
/// Maximum price filter
/// </summary>
[Range(0, double.MaxValue)]
public decimal? MaxPrice { get; set; }
/// <summary>
/// Sort field (name, price, category)
/// </summary>
public string? SortBy { get; set; } = "name";
/// <summary>
/// Sort direction (asc, desc)
/// </summary>
public string? SortDirection { get; set; } = "asc";
/// <summary>
/// Page number
/// </summary>
[Range(1, int.MaxValue)]
public int Page { get; set; } = 1;
/// <summary>
/// Page size (1-100)
/// </summary>
[Range(1, 100)]
public int PageSize { get; set; } = 10;
}
/// <summary>
/// Paged result wrapper
/// </summary>
/// <typeparam name="T">Item type</typeparam>
public class PagedResult<T>
{
/// <summary>
/// The items on the current page
/// </summary>
public List<T> Items { get; set; } = new();
/// <summary>
/// Total number of items across all pages
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Current page number
/// </summary>
public int Page { get; set; }
/// <summary>
/// Number of items per page
/// </summary>
public int PageSize { get; set; }
/// <summary>
/// Total number of pages
/// </summary>
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
}}
8. Real-World E-Commerce API Example
Complete E-Commerce API Structure
Let's build a comprehensive e-commerce API with multiple controllers:
// Controllers/OrdersController.csnamespace ECommerceAPI.Controllers{
/// <summary>
/// Manages order processing and tracking
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Authorize]
[Produces("application/json")]
public class OrdersController : ControllerBase
{
private static readonly List<Order> _orders = new();
private static int _orderIdCounter = 1;
/// <summary>
/// Places a new order
/// </summary>
/// <param name="request">Order details</param>
/// <returns>The created order with tracking information</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<ApiResponse<Order>> PlaceOrder([FromBody] PlaceOrderRequest request)
{
if (!ModelState.IsValid)
{
return BadRequest(new ApiResponse<object>
{
Success = false,
Message = "Invalid order data",
Data = ModelState.Values.SelectMany(v => v.Errors)
});
}
// Validate products exist and have sufficient stock
var validationErrors = ValidateOrderItems(request.Items);
if (validationErrors.Any())
{
return BadRequest(new ApiResponse<object>
{
Success = false,
Message = "Order validation failed",
Data = validationErrors
});
}
var order = new Order
{
Id = _orderIdCounter++,
CustomerId = GetCurrentCustomerId(),
OrderDate = DateTime.UtcNow,
Status = OrderStatus.Pending,
TotalAmount = CalculateOrderTotal(request.Items),
ShippingAddress = request.ShippingAddress,
Items = request.Items.Select(item => new OrderItem
{
ProductId = item.ProductId,
Quantity = item.Quantity,
UnitPrice = GetProductPrice(item.ProductId)
}).ToList(),
TrackingNumber = GenerateTrackingNumber()
};
_orders.Add(order);
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, new ApiResponse<Order>
{
Success = true,
Message = "Order placed successfully",
Data = order
});
}
/// <summary>
/// Retrieves a specific order by ID
/// </summary>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ApiResponse<Order>> GetOrder(int id)
{
var order = _orders.FirstOrDefault(o => o.Id == id);
if (order == null)
{
return NotFound(new ApiResponse<object>
{
Success = false,
Message = $"Order with ID {id} not found"
});
}
// Ensure customers can only see their own orders
if (order.CustomerId != GetCurrentCustomerId() && !User.IsInRole("Admin"))
{
return Forbid();
}
return Ok(new ApiResponse<Order>
{
Success = true,
Message = "Order retrieved successfully",
Data = order
});
}
/// <summary>
/// Updates order status (Admin only)
/// </summary>
[HttpPut("{id}/status")]
[Authorize(Roles = "Admin")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ApiResponse<Order>> UpdateOrderStatus(int id, [FromBody] UpdateOrderStatusRequest request)
{
var order = _orders.FirstOrDefault(o => o.Id == id);
if (order == null)
{
return NotFound(new ApiResponse<object>
{
Success = false,
Message = $"Order with ID {id} not found"
});
}
order.Status = request.NewStatus;
order.UpdatedAt = DateTime.UtcNow;
return Ok(new ApiResponse<Order>
{
Success = true,
Message = "Order status updated successfully",
Data = order
});
}
private int GetCurrentCustomerId()
{
// In real application, get from JWT token
return 1; // Mock customer ID
}
private List<string> ValidateOrderItems(List<OrderItemRequest> items)
{
var errors = new List<string>();
foreach (var item in items)
{
var product = ProductsController.GetProductById(item.ProductId);
if (product == null)
{
errors.Add($"Product with ID {item.ProductId} not found");
}
else if (product.Stock < item.Quantity)
{
errors.Add($"Insufficient stock for product {product.Name}. Available: {product.Stock}, Requested: {item.Quantity}");
}
}
return errors;
}
private decimal CalculateOrderTotal(List<OrderItemRequest> items)
{
return items.Sum(item => GetProductPrice(item.ProductId) * item.Quantity);
}
private decimal GetProductPrice(int productId)
{
var product = ProductsController.GetProductById(productId);
return product?.Price ?? 0;
}
private string GenerateTrackingNumber()
{
return $"TRK{DateTime.UtcNow:yyyyMMddHHmmss}";
}
}
/// <summary>
/// Represents an order in the system
/// </summary>
public class Order
{
/// <summary>
/// Unique order identifier
/// </summary>
public int Id { get; set; }
/// <summary>
/// Customer who placed the order
/// </summary>
public int CustomerId { get; set; }
/// <summary>
/// Date and time when order was placed
/// </summary>
public DateTime OrderDate { get; set; }
/// <summary>
/// Current status of the order
/// </summary>
public OrderStatus Status { get; set; }
/// <summary>
/// Total order amount
/// </summary>
public decimal TotalAmount { get; set; }
/// <summary>
/// Shipping address for the order
/// </summary>
public Address ShippingAddress { get; set; } = new();
/// <summary>
/// List of items in the order
/// </summary>
public List<OrderItem> Items { get; set; } = new();
/// <summary>
/// Tracking number for shipment
/// </summary>
public string TrackingNumber { get; set; } = string.Empty;
/// <summary>
/// Last update timestamp
/// </summary>
public DateTime UpdatedAt { get; set; }
}
/// <summary>
/// Represents an item within an order
/// </summary>
public class OrderItem
{
/// <summary>
/// Product identifier
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Quantity ordered
/// </summary>
public int Quantity { get; set; }
/// <summary>
/// Price per unit at time of order
/// </summary>
public decimal UnitPrice { get; set; }
/// <summary>
/// Total price for this line item
/// </summary>
public decimal LineTotal => Quantity * UnitPrice;
}
/// <summary>
/// Request model for placing a new order
/// </summary>
public class PlaceOrderRequest
{
/// <summary>
/// List of items to order
/// </summary>
[Required]
[MinLength(1)]
public List<OrderItemRequest> Items { get; set; } = new();
/// <summary>
/// Shipping address
/// </summary>
[Required]
public Address ShippingAddress { get; set; } = new();
}
/// <summary>
/// Individual order item request
/// </summary>
public class OrderItemRequest
{
/// <summary>
/// Product identifier
/// </summary>
[Required]
[Range(1, int.MaxValue)]
public int ProductId { get; set; }
/// <summary>
/// Quantity to order
/// </summary>
[Required]
[Range(1, 100)]
public int Quantity { get; set; }
}
/// <summary>
/// Request model for updating order status
/// </summary>
public class UpdateOrderStatusRequest
{
/// <summary>
/// New status for the order
/// </summary>
[Required]
public OrderStatus NewStatus { get; set; }
}
/// <summary>
/// Represents a physical address
/// </summary>
public class Address
{
/// <summary>
/// Street address
/// </summary>
[Required]
public string Street { get; set; } = string.Empty;
/// <summary>
/// City
/// </summary>
[Required]
public string City { get; set; } = string.Empty;
/// <summary>
/// State or province
/// </summary>
[Required]
public string State { get; set; } = string.Empty;
/// <summary>
/// Postal code
/// </summary>
[Required]
public string ZipCode { get; set; } = string.Empty;
/// <summary>
/// Country
/// </summary>
[Required]
public string Country { get; set; } = string.Empty;
}
/// <summary>
/// Possible order status values
/// </summary>
public enum OrderStatus
{
/// <summary>
/// Order has been placed but not processed
/// </summary>
Pending,
/// <summary>
/// Order is being processed
/// </summary>
Processing,
/// <summary>
/// Order has been shipped
/// </summary>
Shipped,
/// <summary>
/// Order has been delivered
/// </summary>
Delivered,
/// <summary>
/// Order was cancelled
/// </summary>
Cancelled,
/// <summary>
/// Order refund was processed
/// </summary>
Refunded
}}
9. Testing APIs with Swagger UI
Interactive Testing Features
Swagger UI provides powerful testing capabilities:
// Enhanced Swagger UI configuration for better testing experience
app.UseSwaggerUI(c =>{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "ECommerce API v1");
// Testing enhancements
c.EnablePersistAuthorization(); // Remember auth tokens
c.EnableFilter(); // Filter endpoints by tag
c.DisplayRequestDuration(); // Show request timing
c.ShowExtensions(); // Show vendor extensions
c.EnableValidator(); // Enable schema validation
c.SupportedSubmitMethods(); // All HTTP methods enabled
// OAuth configuration for testing authenticated endpoints
c.OAuthClientId("swagger-ui");
c.OAuthClientSecret("swagger-ui-secret");
c.OAuthRealm("swagger-ui-realm");
c.OAuthAppName("Swagger UI");
c.OAuthScopeSeparator(" ");
c.OAuthUsePkce();});
Example Test Scenarios
Scenario 1: Testing Product Creation
Navigate to POST /api/Products
Click "Try it out"
Enter request body:
{"name": "Wireless Keyboard","price": 79.99,"category": "Electronics","stock": 25}
Click "Execute"
Review response and status code
Scenario 2: Testing Authentication
Click "Authorize" button
Enter Bearer token (if using JWT)
Test protected endpoints
Scenario 3: Testing Error Cases
Test with invalid data
Test with missing required fields
Verify proper error responses
10. Best Practices and Common Pitfalls
Best Practices
Consistent Documentation
// ✅ Good - Comprehensive documentation/// <summary>/// Creates a new user account/// </summary>/// <param name="request">User registration data</param>/// <returns>The created user with generated ID</returns>/// <response code="201">User created successfully</response>/// <response code="400">Invalid user data</response>
// ❌ Bad - Minimal documentation/// <summary>/// Creates user/// </summary>
Proper Response Types
// ✅ Good - Specific response types[ProducesResponseType(StatusCodes.Status200OK)][ProducesResponseType(StatusCodes.Status404NotFound)][ProducesResponseType(StatusCodes.Status400BadRequest)]
// ❌ Bad - Generic response types[ProducesResponseType(200)]
Security Configuration
// ✅ Good - Proper security setup[Authorize(Roles = "Admin")][HttpDelete("{id}")]public IActionResult DeleteProduct(int id)
// ❌ Bad - Missing authorization[HttpDelete("{id}")]public IActionResult DeleteProduct(int id)
Common Pitfalls and Solutions
Pitfall 1: Missing XML Documentation
<!-- ✅ Solution: Enable XML documentation in csproj --><PropertyGroup><GenerateDocumentationFile>true</GenerateDocumentationFile><NoWarn>$(NoWarn);1591</NoWarn></PropertyGroup>
Pitfall 2: Enum Display Issues
// ✅ Solution: Configure enum handling
builder.Services.AddSwaggerGen(c =>{
c.UseAllOfForInheritance();
c.UseOneOfForPolymorphism();
c.SelectSubTypesUsing(baseType =>
{
return Assembly.GetExecutingAssembly().GetTypes()
.Where(type => type.IsSubclassOf(baseType));
});
// Display enum values as strings
c.MapType<OrderStatus>(() => new OpenApiSchema
{
Type = "string",
Enum = Enum.GetNames(typeof(OrderStatus))
.Select(name => new OpenApiString(name))
.Cast<OpenApiSchema>()
.ToList()
});});
Pitfall 3: Complex Type Serialization
// ✅ Solution: Use proper JSON configuration
builder.Services.Configure<JsonOptions>(options =>{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.WriteIndented = true;});
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;});
11. Alternatives to Swashbuckle
NSwag
// NSwag configuration
builder.Services.AddOpenApiDocument(config =>{
config.Title = "ECommerce API";
config.Version = "v1";
config.Description = "ECommerce API documentation";
config.GenerateEnumMappingDescription = true;
// Add JWT authentication
config.AddSecurity("JWT", Enumerable.Empty<string>(), new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.ApiKey,
Name = "Authorization",
In = OpenApiSecurityApiKeyLocation.Header,
Description = "Type into the textbox: Bearer {your JWT token}."
});
config.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT"));});
// In Configure
app.UseOpenApi();
app.UseSwaggerUi3();
Comparison Table
Feature | Swashbuckle | NSwag |
---|
Installation | Easy | Easy |
Customization | Extensive | Extensive |
Client Generation | Limited | Excellent |
Performance | Good | Better |
Community | Larger | Growing |
.NET Integration | Native | Good |
12. Conclusion
Swagger is an indispensable tool for modern API development in ASP.NET Core. By implementing the techniques covered in this guide, you can:
Create comprehensive, interactive API documentation
Enable seamless API testing and exploration
Implement robust security documentation
Support multiple API versions
Follow best practices for API design
The real-world e-commerce example demonstrates how Swagger can transform complex API ecosystems into well-documented, discoverable services that impress both your team and API consumers.
Remember that great API documentation is not just a technical requirement—it's a communication tool that accelerates development, reduces errors, and enhances collaboration across teams.