API Tasarımında Backward Compatibility Sağlama
Bir API yayınlandığı anda, onu tüketen istemcilerle bir sözleşme (contract) imzalamış olursunuz. Bu sözleşmeyi bozan değişiklikler, mevcut istemcilerin çökmesine, veri kaybına ve itibar kaybına yol açar. Backward Compatibility (Geriye Dönük Uyumluluk), API'nizi geliştirmeye devam ederken eski istemcilerin çalışmaya devam etmesini sağlayan bir dizi prensip ve tekniktir. Bu yazıda, backward compatibility'nin temel prensiplerini, breaking (bozucu) ve non-breaking (bozmayan) değişiklikleri, sürümleme stratejilerini, deprecation sürecini ve .NET'te uygulama yöntemlerini detaylıca ele alacağız.
1. Breaking vs. Non-Breaking Değişiklikler
Backward compatibility'nin temeli, hangi değişikliklerin istemcileri bozduğunu (breaking) ve hangilerinin bozmadığını (non-breaking) bilmektir.
| Değişiklik Türü | Breaking mi? | Açıklama |
|---|---|---|
| Yeni endpoint ekleme | ❌ Hayır | Mevcut istemciler yeni endpoint'i kullanmak zorunda değil. |
| Yeni opsiyonel alan ekleme (yanıt) | ❌ Hayır | Tolerant reader pattern kullanan istemciler yeni alanı görmezden gelir. |
| Yeni opsiyonel alan ekleme (istek) | ❌ Hayır | Mevcut istemciler bu alanı göndermek zorunda değil. |
| Yeni opsiyonel query parametresi | ❌ Hayır | Mevcut çağrılar varsayılan değeri kullanır. |
| Alan ekleme (zorunlu, istek) | ✅ Evet | Mevcut istemciler bu alanı göndermediği için hata alır. |
| Alan kaldırma (yanıt) | ✅ Evet | İstemciler bu alanı okumaya çalışırsa undefined/null alır. |
| Alan tipi değiştirme | ✅ Evet | int → string dönüşümü istemcileri bozar. |
| Alan adı değiştirme | ✅ Evet | İstemciler eski adı arar, bulamaz. |
| Endpoint kaldırma | ✅ Evet | İstemciler 404 alır. |
| HTTP durum kodu değiştirme | ✅ Evet | 200 OK → 201 Created istemci mantığını bozabilir. |
| Varsayılan davranışı değiştirme | ✅ Evet | Örtük davranış değişikliği sürpriz sonuçlar doğurur. |
| Hata mesajı formatını değiştirme | ⚠️ Duruma bağlı | İstemci hata mesajını parse ediyorsa bozucudur. |
Altın Kural: Emin değilseniz, breaking olduğunu varsayın ve sürümleme stratejinizi buna göre planlayın.
2. Additive Change (Ekleme Prensibi)
Backward compatibility'nin en temel prensibi: Mevcut yapıya ekleme yapın, mevcut yapıdan çıkarma yapmayın.
✅ Non-Breaking (Güvenli) Değişiklikler:
json
// V1 Yanıt
{
"id": 123,
"name": "Laptop",
"price": 1500
}
// V1 + Yeni Alan (Non-Breaking)
{
"id": 123,
"name": "Laptop",
"price": 1500,
"currency": "TRY" // Yeni alan eklendi
}
❌ Breaking (Tehlikeli) Değişiklikler:
json
// V1 Yanıt
{
"id": 123,
"name": "Laptop",
"price": 1500
}
// V1 + Alan Kaldırma (Breaking)
{
"id": 123,
"name": "Laptop" // "price" alanı kaldırıldı!
}
3. Tolerant Reader Pattern (Hoşgörülü Okuyucu)
Backward compatibility sadece sunucunun sorumluluğu değildir. İstemciler de Tolerant Reader olmalıdır: Yani, tanımadıkları alanları görmezden gelmeli, eksik alanlara karşı dayanıklı olmalıdır.
❌ Kırılgan İstemci (Fragile Reader):
csharp
// Yanıtın tam olarak beklenen formatta geldiğini varsayar var json = await response.Content.ReadAsStringAsync(); dynamic data = JsonSerializer.Deserialize<dynamic>(json); var price = data.price; // "price" yoksa exception!
✅ Hoşgörülü İstemci (Tolerant Reader):
csharp
// Eksik alanlara karşı varsayılan değer kullanır
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; }
public decimal? Price { get; set; } // Nullable
public string Currency { get; set; } = "TRY"; // Varsayılan değer
}
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var product = JsonSerializer.Deserialize<ProductDto>(json, options);
Tolerant Reader Prensipleri:
-
Yeni alanları görmezden gel (Ignore unknown properties).
-
Eksik alanlar için varsayılan değer kullan.
-
Null değerlere karşı dayanıklı ol.
-
Alan sırasına bağımlı olma.
-
Hata mesajlarını parse etmeye çalışma.
4. Additive Sürümleme Stratejisi
Breaking değişiklikler kaçınılmaz olduğunda, sürümleme devreye girer. En güvenli yaklaşım, kademeli ve additive (eklemeli) bir yol izlemektir.
Adım 1: Yeni Sürümü Paralel Yayınlayın
text
/api/v1/products ← Eski sürüm (çalışmaya devam ediyor) /api/v2/products ← Yeni sürüm (yeni istemciler için)
Adım 2: Eski Sürümü "Deprecated" Olarak İşaretleyin
csharp
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
[MapToApiVersion("1.0")]
public IActionResult GetV1(int id) { ... }
[HttpGet("{id}")]
[MapToApiVersion("2.0")]
public IActionResult GetV2(int id) { ... }
}
Adım 3: Deprecation (Kullanımdan Kaldırma) Başlıkları Ekleyin
csharp
// Sunucu, eski sürümü kullanan istemcilere uyarı döner
context.Response.Headers.Append("Deprecation", "true");
context.Response.Headers.Append("Sunset", "Sat, 31 Dec 2026 23:59:59 GMT");
context.Response.Headers.Append("Link", "</api/v2/products>; rel=\"successor-version\"");
Sunset başlığı, eski sürümün ne zaman tamamen kapatılacağını bildirir. Link başlığı ise yeni sürüme işaret eder.
Adım 4: Geçiş Süresi Tanıyın
İstemcilere yeni sürüme geçmeleri için makul bir süre tanıyın (genellikle 6-12 ay). Bu süre boyunca her iki sürüm de aktif kalır.
Adım 5: Eski Sürümü Kapatın
Sunset tarihinde, eski sürümü kapatın ve istemcilere 410 Gone yanıtı dönün.
csharp
[HttpGet("{id}")]
[MapToApiVersion("1.0")]
public IActionResult GetV1(int id)
{
return StatusCode(410, new ProblemDetails
{
Title = "API v1 artık kullanılmıyor",
Detail = "Lütfen /api/v2/products endpoint'ini kullanın.",
Status = 410
});
}
5. Sürümleme Olmadan Uyumluluk Sağlama Teknikleri
Her zaman yeni bir sürüm oluşturmak gerekmez. Bazı durumlarda, aynı sürüm içinde uyumluluğu koruyabilirsiniz:
A. Alan Yeniden Adlandırma (Rename)
Eski alanı koruyup, yenisini ekleyin:
json
{
"id": 123,
"name": "Laptop", // Eski alan (deprecated)
"productName": "Laptop", // Yeni alan
"price": 1500
}
Bir süre sonra eski alanı kaldırın.
B. Tip Değişikliği (Type Change)
Eski ve yeni tipleri bir arada sunun:
json
{
"price": 1500, // Eski: sayısal
"priceFormatted": "1500 TRY" // Yeni: string
}
C. Davranış Değişikliği için Feature Flag
Yeni davranışı bir feature flag arkasına alın:
csharp
[HttpGet]
public IActionResult GetProducts([FromQuery] bool useNewPricing = false)
{
if (useNewPricing)
return Ok(GetProductsWithNewPricing());
return Ok(GetProductsWithLegacyPricing());
}
İstemciler, hazır olduklarında useNewPricing=true gönderir.
6. API Sözleşmeleri ve Kontrat Testleri
Backward compatibility'yi garanti altına almak için kontrat testleri (contract tests) kritik öneme sahiptir.
A. OpenAPI Şemasını Sürümleyin
Her API sürümü için OpenAPI şemasını saklayın ve yeni sürümün eskisiyle uyumlu olup olmadığını kontrol edin.
B. Otomatik Uyumluluk Kontrolü
CI/CD pipeline'ında, yeni API sürümünün eski sürümle uyumlu olup olmadığını otomatik kontrol eden araçlar kullanın:
bash
# OpenAPI diff araçları oasdiff breaking api-v1.json api-v2.json
C. Kontrat Testleri
İstemci sözleşmesini doğrulayan testler yazın:
csharp
[Fact]
public async Task GetProduct_ReturnsExpectedFields()
{
var response = await _client.GetAsync("/api/v1/products/123");
var json = await response.Content.ReadAsStringAsync();
var product = JsonSerializer.Deserialize<ProductDto>(json);
// Kritik alanların varlığını doğrula
Assert.NotNull(product.Id);
Assert.NotNull(product.Name);
Assert.True(product.Price > 0);
}
7. Deprecation Süreci (Kullanımdan Kaldırma)
Bir API öğesini kullanımdan kaldırmak, dikkatli bir süreç gerektirir.
Aşamalar:
| Aşama | Süre | Yapılacaklar |
|---|---|---|
| 1. Duyuru | Anında | Deprecation politikasını yayınlayın, istemcileri bilgilendirin. |
| 2. Uyarı | 3-6 ay | Deprecation ve Sunset başlıklarını ekleyin, log'larda uyarı verin. |
| 3. Geçiş Desteği | 6-12 ay | Migration rehberi, örnek kodlar ve destek sağlayın. |
| 4. Kapatma | Sunset tarihi | Eski endpoint'i kapatın, 410 Gone dönün. |
Örnek Deprecation Politikası:
text
API v1, 1 Ocak 2025'te kullanımdan kaldırılmıştır. 31 Aralık 2026'ya kadar desteklenecektir. 1 Ocak 2027'de kapatılacaktır. Lütfen /api/v2/products endpoint'ine geçin.
8. .NET'te Backward Compatibility için Araçlar
| Araç | Amaç |
|---|---|
| Microsoft.AspNetCore.Mvc.Versioning | API sürümleme (URL, header, query) |
| Asp.Versioning.Mvc.ApiExplorer | Swagger/OpenAPI ile sürümleme entegrasyonu |
| oasdiff | OpenAPI şemaları arası breaking change tespiti |
| Swashbuckle.AspNetCore | Her sürüm için ayrı Swagger dokümantasyonu |
| Verify | Snapshot testleri ile API yanıtlarını doğrulama |
9. En İyi Pratikler
-
Tolerant Reader Olun: İstemcileriniz yeni alanları görmezden gelsin, eksik alanlara dayanıklı olsun.
-
Additive Change Yapın: Mevcut yapıya ekleme yapın, çıkarma yapmayın.
-
Breaking Değişiklikleri Sürümleyin: Breaking değişiklikler için mutlaka yeni sürüm oluşturun.
-
Deprecation Politikası Belirleyin: Net bir deprecation takvimi ve iletişim planı oluşturun.
-
Sunset ve Deprecation Başlıklarını Kullanın: İstemcileri otomatik olarak bilgilendirin.
-
Kontrat Testleri Yazın: API sözleşmesini otomatik testlerle koruyun.
-
OpenAPI Şemasını Sürümleyin: Her sürümün şemasını saklayın ve karşılaştırın.
-
Dokümantasyonu Güncel Tutun: Her sürüm için ayrı dokümantasyon sağlayın.
-
Geçiş Süresi Tanıyın: İstemcilere yeni sürüme geçmeleri için makul süre verin.
-
İletişim Kurun: Deprecation kararlarını zamanında ve açıkça duyurun.
Sonuç
Backward compatibility, bir API'nin uzun vadeli başarısının anahtarıdır. Breaking değişikliklerden kaçınmak, additive change prensibini benimsemek, tolerant reader pattern'ini uygulamak, sürümleme stratejilerini doğru kullanmak ve net bir deprecation süreci yönetmek, mevcut istemcilerinizi korurken API'nizi geliştirmeye devam etmenizi sağlar. Unutmayın: Bir API yayınlandığında, onu tüketen istemcilerle bir sözleşme imzalamış olursunuz. Bu sözleşmeyi bozan değişiklikler, güven kaybına ve itibar kaybına yol açar. İyi bir API, zaman içinde evrilirken eski dostlarını da yanında taşır.