API Tasarımı & REST Mimarisi 6 dk okuma 3 okunma

REST'te İlişkisel Kaynaklar ve Derinlik Kontrolü

REST API'lerinde ilişkili kaynakların (nested resources) modellenmesi, alt kaynaklar (sub-resources), URL derinliği yönetimi, N+1 problemi, ilişki genişletme parametreleri (expand, include), aşırı derin iç içe yapıların zararları ve bu sorunlara karşı .NET'te uygulama stratejileri ele alınır.

REST'te İlişkisel Kaynaklar ve Derinlik Kontrolü

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.

Tüm yazılar Soru ya da yorumun mu var? Yaz