Ağustos 2026'dan beri .NET 10 üzerinde çok kiracılı (multi-tenant) bir iş platformunun temellerini kuruyorum: İK, müşteriler, katalog, envanter, teknisyenler ve iş emirleriyle birlikte iş akışı; her biri kendi PostgreSQL veritabanına sahip ayrı servisler olarak, tek bir shell arayüzünün arkasında. Platform MVP aşamasında: ortak temeller çalışıyor ve test edildi, iş özelliklerinin çoğu ise hâlâ bunların üzerine yeniden inşa ediliyor. Burada anlatılan dört repository'deki commit'lerin tamamını ben yazdım: hub, ortak yapı taşları, İK servisi ve iş akışı servisi.
Bağlam
Platform, aynı build çıktılarından kurumlara üç şekilde hizmet verecek biçimde tasarlandı: satırların bir kiracı kimliği (tenant id) taşıdığı paylaşımlı SaaS; tek bir kiracıya ayrılmış, servis başına veritabanları; ve on-premise. MVP yalnızca paylaşımlı modeli kapsıyor ve hiçbir servis başka bir servisin veritabanını okumuyor.
Mühendislik problemi
- Tek bir eksik filtre veri sızdırır. Paylaşımlı bir veritabanında kiracı koşulu olmayan tek bir sorgu, bir kurumun kayıtlarını bir başkasına gösterir. İzolasyon, her geliştiricinin bir WHERE koşulunu hatırlamasına bağlı olamaz.
- Servisler yine de birbirlerinden haber almak zorundadır. Değişiklikler mesaj olarak taşınır; mesajlar ise kaybolabilir, mükerrer gelebilir, bozuk olabilir ya da askıya alınmış bir kiracıya gönderilmiş olabilir.
- Onaylar günlerce bekler. Bekleyen bir iş akışı yeniden başlatmalardan sağ çıkmalıdır ve bir çift tıklama onu iki kez devam ettirmemelidir.
Geliştirdiklerim
- Bir iş akışı ve onay motoru, Ağustos ve Eylül 2026'da geliştirildi. Tanımlar veridir: başlangıç, karar, servis eylemi, onay ve bitiş düğümleri, sonradan da zamanlı ve olay tabanlı beklemeler; koşullar kod yerine kısıtlı bir ifade dilinde yazılır. Bir servis kayıt defteri (service registry) ve bir yetenek kataloğu (capability catalog), adresleri iş akışlarının dışında tutar: bir tanım bir iş operasyonunu adlandırır, onu nereden çağıracağını kayıt defteri bilir. Yayımlanmış sürümler değiştirilemez. Onaylayıcılar bir kullanıcıdan ya da bir rolden gelir ve bir panel bunlardan herhangi birini ya da hepsini şart koşabilir. Üstte bir iş emri modülü (görevler, iş dağıtımı, kontrol listeleri, parçalar, zamanlayıcılar, ekler) yer alır; kodda yaklaşık 300 test bulunur.
- Platform temeli. Yedi bağımsız .NET API'si ve DevExtreme modül arayüzleri; her servisin kendi veritabanı ve rolü var ve hepsi bir nginx gateway'in ve bir oturumun yalnızca kullanmasına izin verilen modülleri yükleyen bir shell'in arkasında. Bir Docker Compose yığını RabbitMQ, bir OpenTelemetry collector ve Grafana LGTM ekliyor; tek bir komut da tüm yolu uçtan uca kontrol ediyor.
- Ortak yapı taşları. Kiracı bağlamı, kimlik servisinin OpenIddict ile ürettiği RS256 erişim token'larının doğrulanması, kiracı çözümleme, bir EF Core kiracı filtresi ve kaydetme koruması (save guard), satır düzeyinde güvenlik, sabit kodlu problem+json hataları ve yetki politikaları için NuGet paketleri.
- Güvenilir olaylar. İK bir kaydı ve bir outbox satırını tek bir transaction içinde yazar; bir publisher da satırı confirm mekanizmasıyla RabbitMQ'ya gönderir. Olay adları ve payload'lar bir sürüm taşır. İş akışı servisi, ack göndermeden önce bir inbox satırını ve olayın etkisini tek bir transaction içinde saklar. Sözleşme hataları hemen dead-letter kuyruğuna gider; diğer hatalar 5 saniyeden 30 dakikaya uzanan altı gecikme kademesiyle yeniden denenir.
Tasarım notları
Kiracı izolasyonu için iki koruma
Kiracı yalnızca erişim token'ının tenant_id claim'inden gelir. İstemcinin gönderdiği kiracı header'ı bununla eşleşmelidir, aksi halde istek reddedilir; bilinmeyen bir kiracı varsayılana düşmek yerine güvenli tarafta kalarak reddedilir (fail closed); askıya alınmış ya da kapatılmış kiracılar ise kendi hata kodlarını alır, böylece shell kullanıcıya ne yapması gerektiğini söyleyebilir.
Bir servisin içinde, bir EF Core global query filter sıradan sorguların kapsamını belirler ve bir kaydetme koruması başka bir kiracıya yönelik yazmaları reddeder. PostgreSQL satır düzeyinde güvenlik (row-level security) ikinci korumadır: kısıtlayıcı (restrictive) bir politika her satırın kiracısını, bir bağlantı interceptor'ının güncel tuttuğu bir oturum ayarıyla karşılaştırır. Ham SQL gibi filtrenin yakalayamadığı durumları yakalar. Veritabanı rolü superuser ise ya da satır düzeyinde güvenliği atlayabiliyorsa servis başlamayı reddeder.
"Kaydet, sonra yayımla" yerine outbox ve inbox
Kayıttan sonra yayımlamak, süreç arada durursa olayı kaybeder; önce yayımlamak ise hiç commit edilmeyen bir değişikliği duyurabilir. Outbox, kaydı ve olayını birlikte commit eder; broker da en az bir kez teslim eder. Mesaj id'siyle anahtarlanan inbox, iki tüketici yarışsa bile bunu tek bir etkiye dönüştürür. Mesajlar yalnızca aktif kiracılar için işlenir, aksi halde hiçbir şey saklanmaz; böylece bir dead letter, kiracısı aktif olduğunda yeniden oynatılabilir.
İlk kuyruk olarak veritabanı
İş akışı motoru, kalıcı yürütme kuyruğu olarak ilk başta bir broker yerine PostgreSQL'i kullandı. Worker'lar token'ları bir lease altında üstlenir; bunu uygulama tarafından damgalanan bir eşzamanlılık token'ı (concurrency token) korur ve süresi dolan bir lease token'ı kuyruğa geri döndürür; bu yüzden fazladan worker çalıştırmak güvenlidir. Bu, polling gecikmesi pahasına bir altyapı parçasından tasarruf sağladı. Servislerin olay alışverişi yapması gerektiğinde aralarındaki taşıma katmanı RabbitMQ oldu. Motorun kodu servisin geçmişinde duruyor; iş emirleri ise yeni temel üzerine yeniden inşa ediliyor.
Kiracı etiketi olmayan telemetri
Loglar, trace'ler ve metrikler OpenTelemetry üzerinden Grafana LGTM'ye gider; correlation id ve traceparent mesaj özelliklerinde taşınır, böylece tek bir trace hem publisher'ı hem tüketiciyi kapsar. Kiracı id'leri hiçbir zaman metrik etiketi olmaz; bu da kardinaliteyi kontrol altında tutar.
Test ve CI
- Entegrasyon testleri, Testcontainers içinde gerçek PostgreSQL 16 ve RabbitMQ 4.1 kullanır; test için imzalanmış token'larla superuser olmayan bir rol olarak çalışır ve sözleşmeleri, erişimi, izolasyonu, satır düzeyinde güvenliği, migration'ları ve tüketiciyi kapsar.
- Mimari testler bağımlılık kuralını zorunlu kılar: EF Core'a, Npgsql'e ve RabbitMQ istemcisine yalnızca infrastructure referans verir.
- Bu yazının yazıldığı sırada yapı taşlarında yaklaşık 330, İK'da 130 ve iş akışı servisinde 200 test metodu var.
- Her repository'deki GitHub Actions build alır, formatı kontrol eder, testleri çalıştırır, güvenlik açığı bulunan paketleri tarar, imajları ve arayüzleri derler.
Teknoloji yığını
- .NET 10, ASP.NET Core, Npgsql ile EF Core, FluentValidation
- PostgreSQL 16, satır düzeyinde güvenlik ile
- RabbitMQ 4.1
- OpenIddict tabanlı bir kimlik servisinden RS256 erişim token'ları
- OpenTelemetry, Grafana LGTM
- Shell ve modül arayüzleri için React, DevExtreme, Vite ve TypeScript
- xUnit, Testcontainers, Docker Compose, GitHub Actions
Durum
- Geliştiriliyor, MVP aşamasında; İK ve iş akışı servisleri şimdiden test edilmiş temeller üzerinde çalışıyor.
- İki servis de iş özellikleri (personel, izin ve devam takibi; iş emirleri) yeniden inşa edilirken hâlâ temelin örnek kayıtlarını sunuyor. Ayrılmış (dedicated) ve on-premise kiracılık, kiracı başına yetkilendirmeler (entitlements) ve token iptal olayları planlanıyor. Staging ya da production ortamına hiçbir şey deploy edilmedi.
İlgili
Hamsayeh diğer yolu izliyor: her alan için bir şemaya sahip modüler bir monolit. Outbox deseni .NET'te RabbitMQ yazısında daha ayrıntılı ele alınıyor. .NET üzerinde çok kiracılı bir sistem planlıyorsanız Benimle çalışın sayfasına bakın.
Sık sorulan sorular
Çok kiracılı platform tamamlandı mı?
Hayır. MVP aşamasında. Kiracılık, erişim, mesajlaşma ve gözlemlenebilirlik için ortak temeller kuruldu ve test edildi; İK ve iş emri servislerinin iş özellikleri ise bunların üzerine yeniden inşa ediliyor. Henüz staging ya da production ortamına hiçbir şey deploy edilmedi.
Platform kiracıların verilerini nasıl birbirinden ayırıyor?
Kiracı yalnızca erişim token'ının tenant_id claim'inden gelir ve bilinmeyen kiracılar reddedilir. Her servisin içinde bir EF Core global query filter sıradan sorguları geçerli kiracıyla sınırlar; PostgreSQL satır düzeyinde güvenlik (row-level security) ise ham SQL'i de kapsayan ikinci bir korumadır.
Servisler olayları güvenilir şekilde nasıl paylaşıyor?
Yayımlayan servis verisini ve bir outbox satırını tek bir transaction içinde yazar; bir arka plan publisher'ı da bu satırı confirm mekanizmasıyla RabbitMQ'ya gönderir. Tüketici, ack göndermeden önce bir inbox satırını ve onun etkisini tek bir transaction içinde saklar; idempotency anahtarı olarak mesaj id'sini kullanır, kademeli yeniden denemeler ve bir dead-letter kuyruğu da vardır.
Platform hangi teknolojileri kullanıyor?
ASP.NET Core ve EF Core ile .NET 10, PostgreSQL 16, RabbitMQ 4.1, OpenIddict tabanlı bir kimlik servisinden RS256 erişim token'ları, Grafana LGTM ile OpenTelemetry, DevExtreme modül arayüzleri, Testcontainers ile xUnit, Docker Compose ve GitHub Actions.
Bu bölümdeki diğer sayfalar
- Uyarlamalı Test Platformu: yaklaşık 89.000 öğrenci için IRT tabanlı değerlendirmeler
- Yapay Zekâ ile Soru Aktarımı: sınav PDF'lerinden yapılandırılmış LaTeX maddelerine
- Hamsayeh: binaları ve konut sitelerini yönetmek için bir SaaS
- Teslimat ve Saha Servisi Platformu: siparişler, kuryeler, teknisyenler ve stok tek sistemde
- Sınav Zirvesinde Yüksek Erişilebilirlik: 45.000–70.000 eşzamanlı kullanıcı için Redis Sentinel
- Refah Bankası Döviz Bürosu için Döviz Havalesi Back Office Sistemi
- MBRI için Finansal Gösterge Dashboard'u
Mahdi Molavi