REST API'lerde İlişkisel Kaynaklar ve Derinlik Kontrolü
REST API tasarımının en zorlu kararlarından biri, ilişkili kaynakların nasıl modelleneceğidir. Bir siparişin müşterisi, bir siparişin kalemleri, bir kalemin ürünü, bir ürünün kategorisi... Bu ilişkiler zincirleme olarak devam eder. Peki, bu ilişkileri URI'de ne kadar derine kadar yansıtmalıyız? /customers/123/orders/456/items/789/product gibi bir URI kabul edilebilir mi? Yoksa her kaynağı bağımsız olarak mı modellemeliyiz? Bu yazıda, ilişkisel kaynakların tasarımını, alt kaynak (sub-resource) kullanımını, derinlik kontrolünü ve .NET'te uygulama stratejilerini detaylıca ele alacağız.
1. İlişkisel Kaynak Modelleme Yaklaşımları
İlişkili kaynakları modellemenin üç temel yaklaşımı vardır:
A. Alt Kaynak (Nested / Sub-Resource)
İlişkili kaynak, ana kaynağın URI'si altında modellenir.
text
GET /api/users/123/orders → Kullanıcının siparişleri GET /api/users/123/orders/456 → Kullanıcının belirli bir siparişi POST /api/users/123/orders → Kullanıcı için yeni sipariş
Avantaj: İlişki URI'de açıkça görülür.
Dezavantaj: Derinlik arttıkça URI karmaşıklaşır; kaynak bağımsız erişilemez.
B. Üst Düzey Kaynak (Top-Level Resource) + Filtre
İlişkili kaynak, kendi başına üst düzey bir kaynak olarak modellenir ve filtre parametreleriyle ilişki kurulur.
text
GET /api/orders?userId=123 → Kullanıcının siparişleri GET /api/orders/456 → Sipariş detayı (içinde userId var)
Avantaj: Kaynak bağımsızdır; farklı filtrelerle erişilebilir.
Dezavantaj: İlişki URI'de açıkça görünmez.
C. Hibrit Yaklaşım (En İyi Pratik)
Her iki yaklaşımı birlikte sunun. İstemci, ihtiyacına göre seçer.
text
GET /api/users/123/orders → Kısayol (kullanıcının siparişleri) GET /api/orders?userId=123 → Filtrelenmiş üst düzey kaynak GET /api/orders/456 → Sipariş detayı
2. URL Derinliği: Ne Kadar Derin Olmalı?
URI hiyerarşisi, kaynaklar arasındaki ilişkiyi yansıtır. Ancak aşırı derinlik, API'yi karmaşık ve kullanılamaz hale getirir.
Kabul Edilebilir (2-3 seviye):
text
/api/users/123/orders /api/users/123/orders/456 /api/orders/456/items
Kaçınılması Gereken (4+ seviye):
text
/api/users/123/orders/456/items/789/reviews/101/comments
Neden Aşırı Derinlik Kötüdür?
-
Okunabilirlik Kaybı: URI'ler uzar, anlaşılması zorlaşır.
-
Kırılganlık: Üst kaynaklardan biri değişirse, tüm alt URI'ler etkilenir.
-
Bağımlılık: Alt kaynağa erişmek için tüm üst kaynakların ID'lerini bilmek gerekir.
-
Cache Sorunu: Uzun URI'ler cache anahtarlarını şişirir.
-
Routing Karmaşıklığı: ASP.NET Core'da route tanımları karmaşıklaşır.
Kural: Bir kaynak, üst kaynağa bağımlı olmadan da anlamlıysa, onu üst düzey kaynak olarak modelleyin.
text
❌ /api/users/123/orders/456/items/789 ✅ /api/order-items/789
3. Derinlik Kontrolü Stratejileri
A. Maksimum Derinlik Sınırı Belirleyin
Kurumsal bir kural olarak, URI derinliğini 2-3 seviye ile sınırlayın. Daha derin ilişkiler için üst düzey kaynak kullanın.
B. Bağımsız Kaynak İlkesi
Bir kaynak aşağıdaki özelliklere sahipse, üst düzey kaynak olarak modelleyin:
-
Kendi benzersiz kimliği varsa (
orderItemId) -
Üst kaynağa bağımlı olmadan anlamlıysa
-
Farklı üst kaynaklar altında da görünebiliyorsa
C. İlişki Genişletme (Expansion) Parametreleri
İlişkili veriyi URI'de iç içe göstermek yerine, query parametresi ile genişletin:
text
GET /api/orders/456?expand=customer,items GET /api/orders/456?include=items.product
Bu yaklaşım, URI'yi sade tutarken ilişkili veriye erişim sağlar.
4. N+1 Problemi ve İlişki Yönetimi
İlişkisel kaynaklar, N+1 problemini beraberinde getirir. Örneğin, /api/orders endpoint'i her sipariş için müşteri bilgisini ayrı ayrı çekerse, N+1 sorgu çalışır.
❌ N+1 Problemi:
csharp
[HttpGet]
public async Task<IActionResult> GetOrders()
{
var orders = await _context.Orders.ToListAsync();
var result = orders.Select(o => new OrderDto
{
Id = o.Id,
CustomerName = _context.Customers.Find(o.CustomerId).Name // Her sipariş için ayrı sorgu!
});
return Ok(result);
}
✅ Çözüm: Eager Loading (Include)
csharp
[HttpGet]
public async Task<IActionResult> GetOrders([FromQuery] string? expand)
{
var query = _context.Orders.AsNoTracking();
// İlişkili veriyi önceden yükle
if (expand?.Contains("customer") == true)
query = query.Include(o => o.Customer);
if (expand?.Contains("items") == true)
query = query.Include(o => o.Items);
var orders = await query.ToListAsync();
return Ok(_mapper.Map<List<OrderDto>>(orders));
}
5. İlişki Genişletme (Expansion) Tasarımı
İlişkili verinin nasıl genişletileceğini kontrol etmek, API'nizin esnekliğini artırır.
A. expand Parametresi (Basit)
text
GET /api/orders/456?expand=customer GET /api/orders/456?expand=customer,items
B. include Parametresi (İç İçe İlişkiler)
text
GET /api/orders/456?include=items.product GET /api/orders/456?include=items.product.category
C. fields Parametresi (Alan Seçimi)
text
GET /api/orders/456?fields=id,total,status
D. Hibrit Kullanım
text
GET /api/orders/456?expand=customer&fields=id,total,customer.name
Güvenlik Uyarısı: Kullanıcı tarafından gönderilen include parametrelerini doğrulamadan kullanmayın. Aksi takdirde, kötü niyetli bir kullanıcı çok derin iç içe yüklemeler yaparak sunucuyu çökertebilir (DoS saldırısı).
csharp
// Beyaz liste ile doğrulama
private static readonly HashSet<string> AllowedIncludes = new()
{
"customer", "items", "items.product"
};
public async Task<IActionResult> GetOrder(int id, [FromQuery] string? include)
{
var includes = include?.Split(',') ?? Array.Empty<string>();
var invalidIncludes = includes.Except(AllowedIncludes);
if (invalidIncludes.Any())
return BadRequest($"Geçersiz include parametresi: {string.Join(", ", invalidIncludes)}");
// ... güvenli include işlemi
}
6. DTO Tasarımı ile İlişki Yönetimi
İlişkili kaynakların DTO tasarımı, API'nizin kullanılabilirliğini belirler.
A. Minimal Temsil (Referans by ID)
csharp
public class OrderDto
{
public int Id { get; set; }
public decimal Total { get; set; }
public int CustomerId { get; set; } // Sadece ID
}
B. Genişletilmiş Temsil (Embedded)
csharp
public class OrderDto
{
public int Id { get; set; }
public decimal Total { get; set; }
public int CustomerId { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CustomerDto? Customer { get; set; } // expand=customer ise dolu
}
C. Koleksiyon İlişkileri
csharp
public class OrderDto
{
public int Id { get; set; }
public decimal Total { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<OrderItemDto>? Items { get; set; } // expand=items ise dolu
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? ItemCount { get; set; } // Özet bilgi (her zaman dönebilir)
}
7. İlişkisel Kaynaklar için En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| Maksimum 2-3 seviye derinlik | Daha derin ilişkiler için üst düzey kaynak kullanın. |
| Bağımsız kaynak ilkesi | Kaynak kendi başına anlamlıysa, üst düzey kaynak yapın. |
| Hibrit yaklaşım | Alt kaynak + filtrelenmiş üst düzey kaynak birlikte sunun. |
expand / include parametreleri |
İlişkili veriyi URI'de değil, query parametresiyle genişletin. |
| Beyaz liste doğrulaması | include parametrelerini mutlaka doğrulayın (DoS koruması). |
| Eager Loading | N+1 problemini Include ile çözün. |
| Minimal + Genişletilmiş temsil | Varsayılan olarak minimal, expand ile genişletilmiş veri dönün. |
| Tutarlılık | Tüm API'de aynı derinlik ve genişletme kurallarını uygulayın. |
| Dokümantasyon | Hangi ilişkilerin desteklendiğini ve nasıl genişletileceğini belgeleyin. |
8. Gerçek Dünya Örneği: E-Ticaret API'si
İyi Tasarlanmış URI Yapısı:
text
GET /api/v1/orders → Tüm siparişler GET /api/v1/orders?userId=123 → Kullanıcının siparişleri GET /api/v1/orders/456 → Sipariş detayı (minimal) GET /api/v1/orders/456?expand=customer,items → Genişletilmiş sipariş detayı GET /api/v1/users/123/orders → Kullanıcının siparişleri (kısayol) GET /api/v1/order-items/789 → Sipariş kalemi (bağımsız kaynak) GET /api/v1/order-items/789/product → Kalemin ürünü (2 seviye)
Kaçınılması Gereken URI Yapısı:
text
❌ GET /api/v1/users/123/orders/456/items/789/product/category/101/reviews
Bu URI, 6 seviye derinliğindedir ve okunamaz durumdadır. Doğru yaklaşım:
text
✅ GET /api/v1/products/789 ✅ GET /api/v1/products/789/category ✅ GET /api/v1/products/789/reviews
Sonuç
REST API'lerinde ilişkisel kaynakların modellenmesi, API'nizin uzun vadeli başarısını belirleyen kritik bir karardır. Alt kaynaklar (nested resources) ilişkiyi net bir şekilde gösterirken, üst düzey kaynaklar bağımsızlık ve esneklik sunar. En iyi yaklaşım, hibrit model kullanmak ve maksimum 2-3 seviye derinlik ile sınırlamaktır. expand ve include gibi parametrelerle ilişkili veriyi URI'yi kirletmeden genişletebilir, beyaz liste doğrulaması ile güvenliği sağlayabilir, Eager Loading ile N+1 problemini çözebilirsiniz. Unutmayın: İyi bir REST API, bir veritabanı şemasının HTTP üzerinden yansıması değil, iş alanının doğal ve kullanılabilir bir modellemesidir.