ASP.NET Core Web API'de Swagger ve OpenAPI: Swashbuckle ile Dokümantasyon
Swashbuckle ile ASP.NET Core Web API'ye OpenAPI belgesi ve Swagger UI ekliyoruz: doğru paket sürümü, XML comments, response tiplerini belgelemek, JWT için Authorize butonu ve gerçek hata mesajlarıyla sık düşülen tuzaklar.
Sürüm notu: Bu yazıdaki kodlar .NET 6 ile yazılmıştır ve .NET 10'da da değişmeden çalışır.
Önce Tahmin Et
Bir önceki yazıda API'mize sürüm numarası verdik ve eski istemcileri kırmadan yeni endpoint'ler açmayı gördük. Güzel, ama bir sorun kaldı: frontend ekibindeki arkadaşın hangi endpoint'in hangi body'yi beklediğini, hangi status code'ları dönebileceğini nereden öğrenecek? Controller'ı açıp kodu okuyarak değil, umarım.
Bu yazıda o sorunun standart cevabını kuracağız: OpenAPI belgesi ve onu ekranda gezilebilir hâle getiren Swagger UI. Başlamadan küçük bir soru. Birazdan yazacağımız kodla uygulamayı Development'ta çalıştırıp /swagger adresini açınca dokümantasyon sayfası geliyor. Aynı uygulamayı ASPNETCORE_ENVIRONMENT=Production ile ayağa kaldırıp aynı adrese gidersen ne görürsün? Cevabı yazının ortasında.
OpenAPI, Swagger ve Swashbuckle: Kim Kimdir?
Bu üç isim sürekli birbirinin yerine kullanılıyor, o yüzden önce ayıralım. OpenAPI bir spesifikasyon: bir HTTP API'sinin endpoint'lerini, parametrelerini, request ve response şemalarını JSON ya da YAML ile tarif etmenin standart biçimi. Swagger ise bu spesifikasyonun eski adı ve bugün etrafındaki araçların markası. Swagger UI, bir OpenAPI belgesini okuyup tarayıcıda deneme yapılabilen bir sayfaya çeviren araç.
Swashbuckle da işin ASP.NET Core tarafı. Uygulama çalışırken controller'larını, route'larını ve dönüş tiplerini inceliyor, bunlardan bir swagger.json üretiyor ve içinde paketlenmiş Swagger UI'ı sunuyor. Bilgiyi kendisi uydurmuyor; ASP.NET Core'un ApiExplorer denen katmanının topladığı metadata'yı okuyor. Bu yüzden belgenin kalitesi, senin controller'a ne kadar bilgi verdiğine bağlı. Restoran benzetmesiyle: mutfak senin kodun, OpenAPI belgesi menü, Swagger UI da menünün masadaki basılı hâli. Swashbuckle menüyü mutfağa bakarak yazan garson; mutfakta ne pişiyorsa onu yazar, gerisini sen söylersen ekler.
Swashbuckle'ı Kurmak ve Doğru Sürümü Seçmek
.NET 6 ile dotnet new webapi dediğinde Swashbuckle zaten şablonda gelir; şablonun koyduğu sürüm 6.2.3. Elle ekliyorsan sürümü mutlaka kendin seç. csproj şöyle olsun:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- XML comment dosyasını üret, eksik yorum uyarısını (CS1591) sustur -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
</ItemGroup>
</Project>
Neden 7.3.2? NuGet'teki paket içeriklerine baktığında tablo net: 6.x ve 7.x hatları net6.0'ı doğrudan hedefliyor, 7.3.2 bunu yapan son sürüm. 8.0 ile net6.0 listeden çıkıyor, 10.x ise net8.0, net9.0 ve net10.0 hedefliyor. 7.3.2'yi net10.0 projesine koyduğunda da sorunsuz çalışıyor; bu yazının kodunu iki hedefte de bu sürümle derleyip çalıştırdık.
Sürümü yazmadan dotnet add package Swashbuckle.AspNetCore dersen ne olur? NuGet en yeni sürümü, yani 10.3.1'i ekler ve "compatible" der. Ama o sürümde net6.0 için bağımlılık grubu olmadığından pratikte hiçbir assembly gelmez ve build şöyle patlar:
Program.cs(4,18): error CS1061: 'IServiceCollection' does not contain a definition for 'AddSwaggerGen' and no accessible extension method 'AddSwaggerGen' accepting a first argument of type 'IServiceCollection' could be found (are you missing a using directive or an assembly reference?)
Paket görünüşte yüklü, method'lar yok. Bu hatayı görürsen ilk bakacağın yer csproj'deki sürüm.
Şimdi Program.cs:
using System.Reflection;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// Minimal API endpoint'lerini de ApiExplorer'a tanıtır
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "ProductCatalog API",
Version = "v1",
Description = "Products, categories and orders."
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
// /swagger/v1/swagger.json ve /swagger arayüzü
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapControllers();
app.Run();
AddSwaggerGen belge üreticisini DI'a kaydeder, SwaggerDoc da "v1" adında bir belge tanımlar. Buradaki ad, URL'deki /swagger/v1/swagger.json parçasıyla aynı olmak zorunda. UseSwagger JSON'u, UseSwaggerUI arayüzü sunan middleware'ler.
swagger.json'a Bakmak
Kullanacağımız controller tanıdık: ProductCatalog API'nin ProductsController'ı, şimdilik in-memory bir listeyle.
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
namespace ProductCatalog.Api.Controllers;
/// <summary>
/// Manages the products in the catalog.
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
private static readonly List<ProductDto> Products = new()
{
new ProductDto { Id = 1, Name = "Mechanical Keyboard", Price = 1450m },
new ProductDto { Id = 2, Name = "Wireless Mouse", Price = 650m }
};
/// <summary>
/// Returns a single product by its id.
/// </summary>
/// <param name="id">The product id.</param>
/// <response code="200">The product was found.</response>
/// <response code="404">There is no product with this id.</response>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public ActionResult<ProductDto> GetById(int id)
{
var product = Products.FirstOrDefault(p => p.Id == id);
if (product is null)
{
return NotFound();
}
return product;
}
/// <summary>
/// Adds a new product to the catalog.
/// </summary>
/// <response code="201">The product was created.</response>
/// <response code="400">The request body is invalid.</response>
[HttpPost]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<ProductDto> Create(CreateProductDto dto)
{
var product = new ProductDto
{
Id = Products.Max(p => p.Id) + 1,
Name = dto.Name,
Price = dto.Price
};
Products.Add(product);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
public class CreateProductDto
{
/// <summary>
/// Display name of the product.
/// </summary>
/// <example>Mechanical Keyboard</example>
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Unit price in TRY.
/// </summary>
/// <example>1450</example>
[Range(0.01, 1_000_000)]
public decimal Price { get; set; }
}
Uygulamayı çalıştırıp /swagger'ı açtığında Products başlığı altında iki endpoint görürsün; "Try it out" ile isteği doğrudan sayfadan atabilirsin. Asıl ürün ise arkadaki JSON. curl http://localhost:5161/swagger/v1/swagger.json çıktısında GET endpoint'inin kısmı şöyle:
"/api/Products/{id}": {
"get": {
"tags": [
"Products"
],
"summary": "Returns a single product by its id.",
"parameters": [
{
"name": "id",
"in": "path",
"description": "The product id.",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "The product was found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductDto"
}
}
}
},
"404": {
"description": "There is no product with this id.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
Bu JSON sadece insanlar için değil. NSwag, Kiota ya da openapi-generator gibi araçlar bu dosyadan TypeScript veya C# istemcisi üretebiliyor; Postman da doğrudan içe aktarabiliyor. Belge ne kadar doğruysa, üretilen istemci de o kadar doğru olur.
Tahminin cevabı da burada. Aynı uygulamayı Production ortamında çalıştırınca:
curl -i http://localhost:5163/swagger/index.html
HTTP/1.1 404 Not Found
Content-Length: 0
Date: Tue, 03 Nov 2026 21:15:12 GMT
Server: Kestrel
Swagger middleware'lerini IsDevelopment() bloğuna koyduğumuz için production'da ne JSON var ne arayüz. Bu bir hata değil, bilinçli bir tercih: API'nin bütün yüzeyini herkese açık bir haritayla sunmak istemeyebilirsin. Production'da da istiyorsan blok dışına alırsın, ama o zaman en azından erişimi kısıtlamayı düşün.
XML Comments ile Açıklama Eklemek
Yukarıdaki JSON'daki "Returns a single product by its id." cümlesi kendiliğinden gelmedi. Kaynağı controller'daki /// yorumları. Bu yorumların belgeye girmesi için iki adım gerekiyor. İlki csproj'deki GenerateDocumentationFile: derleyici build sırasında ProductCatalog.Api.xml dosyasını üretip DLL'in yanına koyuyor. NoWarn satırındaki 1591 ise yorum yazmadığın her public üye için çıkan CS1591 uyarısını susturuyor; yoksa build çıktın uyarıdan geçilmez. İkinci adım Swashbuckle'a dosyanın yerini söylemek:
// AddSwaggerGen içinde, SwaggerDoc'tan sonra
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFile));
<summary> özet satırına, <param> parametre açıklamasına, <response code="..."> o status code'un açıklamasına dönüşüyor. DTO property'lerindeki <example> da şemaya örnek değer olarak giriyor; Swagger UI'da "Try it out" dediğinde body kutusu "Mechanical Keyboard" ve 1450 ile dolu geliyor. Data Annotations da boşa gitmiyor: [Required], [StringLength(100)] ve [Range] şemada required, maxLength, minimum ve maximum olarak görünüyor. Yani doğrulama kuralların hem sunucuda çalışıyor hem de belgede yazıyor.
Bu yorumların İngilizce olmasının sebebi, artık sadece kod yorumu olmamaları. Belgeye, dolayısıyla istemciye giden metin bunlar.
Response Tiplerini Belgelemek
[ProducesResponseType] satırlarını silip belgeye tekrar bakarsan ilginç bir şey görürsün. 404 hâlâ listede, çünkü <response code="404"> yorumu ona bir açıklama yazdırıyor; ama altında content yok, yani belge gövdenin ProblemDetails olduğunu artık bilmiyor. Yorumları da silersen GET için tek bir response kalır: "200", açıklaması "OK". Swashbuckle dönüş tipini ActionResult<ProductDto>'tan çıkarabiliyor, ama action'ın içinde bir yerde NotFound() döndüğünü bilemiyor. Kodu çalıştırmıyor, sadece imzaya bakıyor.
Bu yüzden dönebilecek her status code'u attribute ile söylüyoruz. typeof(ProblemDetails) vermemiz de önemli: [ApiController] 404 ve 400 durumlarında ProblemDetails gövdesi ürettiği için belge gerçeği anlatıyor. Controller seviyesindeki [Produces("application/json")] da response içeriğini tek bir media type'a indiriyor; yoksa belgede her response için text/plain ve text/json da listeleniyor.
Her action'a aynı 400 ve 500 satırlarını yazmak sıkıcıysa, attribute'ları controller seviyesine çıkarabilir ya da global bir filter olarak ProducesResponseTypeAttribute ekleyebilirsin. Ama başlangıç için kural basit: action'ın gövdesinde gördüğün her return türünün belgede bir karşılığı olsun.
JWT ile Swagger UI'da Authorize Butonu
JWT yazısında endpoint'leri [Authorize] ile koruduk. Swagger UI'dan bu endpoint'leri denemek istersen her istekte Authorization header'ı gitmeli. Bunu UI'ya öğretmek için belgeye bir security scheme ekliyoruz:
// AddSwaggerGen içinde
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "Paste the token only, without the Bearer prefix."
});
// Tanımı tüm operation'lara uygula
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
İlk çağrı "Bearer adında, header'dan giden, HTTP bearer tipinde bir kimlik doğrulama yöntemi var" diyor. İkincisi bu yöntemi bütün operation'lar için gerekli ilan ediyor. Sonuçta sayfanın sağ üstünde bir Authorize butonu belirir. Token'ı oraya yapıştırırsın, UI her isteğe Authorization: Bearer ... header'ını kendisi ekler. Type = SecuritySchemeType.Http seçtiğimiz için "Bearer " önekini senin yazmaman gerekiyor; açıklamaya bu yüzden o notu düştük. Eski blog yazılarında sık gördüğün SecuritySchemeType.ApiKey varyantında ise öneki elle yazman beklenir.
Belgede bu tanım components.securitySchemes altında "type": "http", "scheme": "bearer" olarak görünür. Bunun sadece bir dokümantasyon ayarı olduğunu unutma: asıl koruma hâlâ AddJwtBearer ve [Authorize]'da. Swagger'a güvenlik tanımı eklemek hiçbir endpoint'i korumaz.
Sık Düşülen Tuzaklar
XML dosyası yoksa sadece Swagger bozulmaz. GenerateDocumentationFile'ı kapatıp IncludeXmlComments'i bıraktığında Development'ta System.IO.FileNotFoundException alırsın, ve bunu yalnızca /swagger'da değil, /api/products/1 dahil her istekte 500 olarak görürsün; çünkü Swagger middleware'i her istekte belge üreticisini DI'dan istiyor. Production'da ise middleware çalışmadığı için hiçbir şey olmaz, bu da hatayı ortamdan ortama gizler.
Aynı isimli iki class farklı namespace'lerde durursa, örneğin Controllers.ProductDto ve Admin.ProductDto, swagger.json 500 döner ve içinde şu satır yazar:
System.InvalidOperationException: Can't use schemaId "$ProductDto" for type "$ProductCatalog.Api.Controllers.ProductDto". The same schemaId is already used for type "$ProductCatalog.Api.Admin.ProductDto"
Swashbuckle şema adını varsayılan olarak class adından alıyor. En hızlı çözüm options.CustomSchemaIds(type => type.FullName);, daha temizi ise DTO'lara çakışmayan isimler vermek.
HTTP method attribute'u olmayan public bir action da belgeyi düşürür: Ambiguous HTTP method for action - ProductCatalog.Api.Controllers.AdminProductsController.Recalculate. Controller'da public ama endpoint olmayan bir method'un varsa [NonAction] ile işaretle ya da private yap.
Bir de nullable meselesi var. Belgede ProductDto.name için "nullable": true yazıyor, oysa C# tarafında string nullable değil. Swashbuckle varsayılan olarak nullable reference type bilgisini okumaz; options.SupportNonNullableReferenceTypes() ile açabilirsin. Üretilen TypeScript istemcisinde her alanın string | null olmasının sebebi genelde budur.
Swashbuckle'ın Bugünü ve .NET 9 Sonrası
Bu yazıdaki yapı, .NET 5'ten .NET 8'e kadar webapi şablonunun varsayılanıydı. Mart 2024'te ASP.NET Core ekibi Swashbuckle'ın bakımının aksadığını duyurdu ve .NET 9 şablonundan çıkardı; yerine çerçevenin kendi belge üreticisi geldi. Swashbuckle'ın bakımı sonradan tekrar canlandı ve 10.x hattı .NET 10'u destekliyor. Yalnız 10.x, altındaki Microsoft.OpenApi kütüphanesinin 2.x sürümüne geçti. Bizim kodumuzu Swashbuckle 10.3.1 ile net10.0'da derlediğinde ilk satırdan düşüyor:
Program.cs(2,25): error CS0234: The type or namespace name 'Models' does not exist in the namespace 'Microsoft.OpenApi' (are you missing an assembly reference?)
Security tanımı da o sürümde farklı yazılıyor. Bu yüzden Blok A boyunca 7.3.2'de kalıyoruz. Yerleşik OpenAPI'yi, Swagger UI yerine Scalar kullanmayı ve Swashbuckle 10'a geçişi .NET 9 ve .NET 10 yazılarında ayrıntılı göreceğiz.
Küçük Bir Deneme
★☆☆ IncludeXmlComments çağrısına ikinci argüman olarak includeControllerXmlComments: true ver. swagger.json'ın en altında tags altında controller'ın özet cümlesini gör, sonra Swagger UI'da Products başlığının yanında aynı cümleyi ara.
★★☆ ProductsController'a [HttpDelete("{id:int}")] ile bir Delete action'ı ekle. 204 ve 404 dönsün. Önce hiçbir attribute ve yorum koymadan belgeye bak, sonra [ProducesResponseType] ve <response> yorumlarını ekleyip farkı karşılaştır.
★★★ JWT yazısındaki login endpoint'ini bu projeye taşı, bir endpoint'i [Authorize] ile koru. Swagger UI'da Authorize'a token'ı yapıştırmadan ve yapıştırdıktan sonra aynı isteği at; tarayıcının geliştirici araçlarında giden header'ı kontrol et. Ardından AddSecurityRequirement'ı silip kilit simgelerinin nasıl kaybolduğuna bak.
Belgemiz hazır, istemciler artık neyi çağıracaklarını biliyor. Peki aynı ürün listesini dakikada binlerce kez isteyen bir istemciye her seferinde veritabanına gidip aynı cevabı mı hesaplayacağız? Bir sonraki yazıda caching'e geçiyoruz: IMemoryCache, Redis ile IDistributedCache ve response caching middleware.