ASP.NET Core  

ASP.NET Core Swagger Mastery: Interactive API Documentation Guide (Part-15 of 40)

 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)


Table of Contents

  1. Introduction to Swagger/OpenAPI

  2. Setting Up Swagger in ASP.NET Core

  3. Basic Configuration and Customization

  4. API Documentation with XML Comments

  5. Advanced Swagger Configuration

  6. Security and Authentication

  7. Versioning with Swagger

  8. Real-World E-Commerce API Example

  9. Testing APIs with Swagger UI

  10. Best Practices and Common Pitfalls

  11. Alternatives to Swashbuckle

  12. 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:

  • Swagger JSON: https://localhost:7000/swagger/v1/swagger.json

  • Swagger 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"]))
        };
    });

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

  1. Navigate to POST /api/Products

  2. Click "Try it out"

  3. Enter request body:

{"name": "Wireless Keyboard","price": 79.99,"category": "Electronics","stock": 25}
  1. Click "Execute"

  2. Review response and status code

Scenario 2: Testing Authentication

  1. Click "Authorize" button

  2. Enter Bearer token (if using JWT)

  3. Test protected endpoints

Scenario 3: Testing Error Cases

  1. Test with invalid data

  2. Test with missing required fields

  3. Verify proper error responses

10. Best Practices and Common Pitfalls

Best Practices

  1. 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>
  1. Proper Response Types

// ✅ Good - Specific response types[ProducesResponseType(StatusCodes.Status200OK)][ProducesResponseType(StatusCodes.Status404NotFound)][ProducesResponseType(StatusCodes.Status400BadRequest)]

// ❌ Bad - Generic response types[ProducesResponseType(200)]
  1. 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

FeatureSwashbuckleNSwag
InstallationEasyEasy
CustomizationExtensiveExtensive
Client GenerationLimitedExcellent
PerformanceGoodBetter
CommunityLargerGrowing
.NET IntegrationNativeGood

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.