Multi-Tenant Architecture: ASP.NET Core ile SaaS Uygulamalar Geliştirmek

Murat Dinç
Devops Türkiye☁️ 🐧 🐳 ☸️
7 min readDec 14, 2022

Selamlar,

Açılımı hizmet olarak yazılım olan SaaS (Software as a Service), birçok kişi tam olarak ne olduğunu bilmemesine rağmen, SaaS yazılımı hemen hemen herkesin kullandığı hizmetler arasındadır.

SaaS uygulamalar ile ilgili araştırma yaptığımızda Multi-Tenant kavramıda sıkça karşılaştığımız bir kavram oldu.

Multi-Tenant bir yazılım uygulamasının tek bir örneğinin birden çok müşteriye hizmet verdiği bir mimaridir. Her kullanıcıya Tenant adı verilir. Bu mimaride bir uygulamanın birden çok örneği aynı ortamda çalışır. Bu mimaride ayrım fiziksel olarak değil mantıksal olarak yapılmaktadır.

Bu mimariyi bir apartman gibi düşünebiliriz. Apartman bize ait ve daireleri kiraya veriyoruz. Kiraya verdiğimiz dairelerin her birinin bir Tenant olduğunu söyleyebiliriz. Her daire ısınma, alan vb. gibi bir çok konuda binanın kaynaklarını paylaşmaktadır.

Multi-Tenant mimarisinin sanallaştırma ile karıştırılmaması gerekmektedir. Bu mimaride olarak geliştirilen bir uygulamada birden çok kullanıcı aynı uygulamayı, aynı işletim ortamında, aynı donanım üzerinde, aynı depolama mekanizmasıyla paylaşır fakat sanallaştırmada her uygulama kendi işletim sistemine sahip ayrı bir sanal makinede çalışır ve bu sebepten kaynaklı sanallaştırma ile arasında ciddi farklar mevcuttur.

https://codewithmukesh.com/blog/multitenancy-in-aspnet-core/

Veri Tabanı Modelleme Çeşitleri

- Multiple Databases: Single Database per Tenant

Her bir kullanıcı için bir veri tabanı oluşturmak üzerine yaklaşılan bir modeldir. Bu modelin en büyük avantajı veri güvenliği sağlamaktır. Her kullanıcının verileri kendi için oluşturulan veri tabanında tutulmaktadır ve başka kullanıcılar bu veri tabanına erişim sağlayamayacağı için veri güvenliği en üst düzeyde olan modeldir.

Bu modelin avantajlarından bir diğeri ise kolay geri dönüşüm yapılabilmesidir. Elinizde olan bir yedek dosyası ile sadece o kullanıcı için bir yedekten dönebilir hızlıca işlem yapabilirsiniz.

Bu faydalar ile birlikte gelen problem ise yönetim problemidir kullanıcı sayınız arttıkça veri tabanı sayınızda artacak olup yönetim problemleri ile karşı karşıya kalabilirsiniz.

- Single Database with Separate Schema per Tenant

Bu modelde tek bir veri tabanı vardır fakat her bir kullanıcı için bir şema oluşturulur. Aynı veri tabanı üzerinde farklı şemalar üzerinden ayrım sağlanır. Aynı tablolar farklı şemalarda her bir kullanıcı için oluşur. Bu model ile gelen en büyük dezavantaj bir kullanıcının verilerinde bir bozulma meydana geldi ise geri dönüşümü için sıkıntılı bir süreç yaşanabilir. Bu modeli genelde bakım maliyetlerinden kaçmak için kullanabiliriz.

- Single Database with Shared Schema

Bu model Single-Tenant modeli ile benzer bir modeldir. Tek bir veri tabanı üzerinden ve paylaşımlı şemalar üzerinden tablolar üzerinde ayrım sağlayarak kullanılan bir modeldir. Bütün kullanıcılar için tablo ve şemalar aynıdır ve hepsi aynı yerleri kullanırlar. Veri almak için TenantId gibi bir yaklaşım ile tek tablo üzerinden kullanıcıya ait verileri alabilirsiniz. Bu modelin en büyük dezavantajı ise zamanla veri tabanı boyutu arttıkça sorgular daha maliyetli hale gelecektir.

Erişim Çeşitleri

Multi-Tenant uygulanmış bir projeye nasıl erişim verebileceğinize dair kaç yol mevcuttur.

  • URL Based: Bu erişim yönteminde kullanıcılarınızın benzersiz kullanıcı adlarına göre bir subdomain oluşturma yöntemine gidebilirsiniz. Örnek olarak user1.testapp.com, user2.testapp.com vb gibi bir kurgu ile kullanıcılarınızın url üzerinden ayrıma gitmesini sağlayabilirsiniz.
  • Query String: Query string yöntemi ile tenant ayrımına gidebilirsiniz. Oluşturduğunuz URL üzerinde ?tenantId=1 vb gibi bir ayrıma giderek erişim sağlayabilirsiniz
  • Claims: Oturuma göre ilgili Tenant erişimini sağlayabileceğiniz bir yöntemdir. Aslında en yaygın kullanım da diyebiliriz.
  • Headers: Header parametreleri üzerinden Tenant ya da vb. bir parametre göndererek istek içerisinde ayırışıma gidebileceğiniz bir yöntemdir. Internal api kullanımlarında tercih edilebilecek bir yöntemdir.
  • Request IP Address: Her kullanıcı için bir sabit bir ip bloğu tanımlanır ve bu ip bilgileri üzerinden Tenant tespit edilir. Örnek olarak yazılım şirketi 185.41.211.10 gibi bir ip adresini X şirketine aittir diye tanımladı o ip adresinden istek geldiği zaman uygulamanın kime ait olduğu ortaya çıkıyor.

En yaygın olarak kullanılan yöntem Claims yöntemidir.

Fakat bu modellerden sadece bir tanesini kullanmak yerine Hybrid olarak da kullanım sağlanabilir.

Örnek Proje

Multi-Tenant uygulayarak projemizi SaaS CRM uygulaması olarak geliştireceğiz. Sistemin işleyişi hakkında ufak bir grafik hazırladım bütün yapıyı aşağıda şema üzerinden ilerleteceğiz. Projemizi Single Database per Tenant veri tabanı modellemesi ile geliştireceğiz. Her bir kullanıcı için bir veritabanı olacaktır.

Gereksinimler

  • .NET Core 6 SDK
  • PostgreSQL
  • Docker
  • Postman
Örnek projemizde kullanacağımız yol haritamız

Sistemi planlamaya başlarken ilk önce bütün kullanıcıları kapsayan ana bir veri tabanı yapmamız gerekiyor bunun için ise tablolar aşağıdaki gibidir. Veri tabanı olarak PostgreSQL tercih ettik.

MultiTenantMainDB adında bir veri tabanımız mevcut ve bütün kullanıcı şirket bilgilerini bu veri tabanı üzerinde tutuyoruz. Aslında bizim için bütün bilgileri sağlayacak olan bir veri tabanı diyebiliriz.

Projeyi bilgisayarımıza çektikten sonra ilgili dizindeyken docker compose up komutunu kullanarak ortamı hazır hale getiriyoruz.

Ortam hazır hale geldikten sonra veri tabanına bağlanmak isterseniz aşağıdaki bilgileri kullanarak bağlantı sağlayabilirsiniz. Ben Management Studio ile bağlantıyı yaptım siz kullandığınız başka bir program var ise onunla yapabilirsiniz

.

Erişim sağladıktan sonra bütün veri tabanları oluşmuş olacaktır

Gelelim veri tabanımızda hangi tablo ne iş yapıyor sorusunu cevaplamaya.

MultiTenantMainDB veri tabanımız için genel Diagram

Company: Sistemimize kayıtlı olan şirketlerin tutulduğu tablo.

Company_User_Mapping: Hangi kullanıcıların şirket üzerinde erişim yetkilerinin olduğunun bilgisinin tutulduğu tablodur.

Pool: Aslında bizim veri tabanı havuzumuz olmaktadır. Veri tabanı sunucularımızı bu tabloda tutuyoruz. Projemizin veri tabanları her zaman tek sunucu üzerinde durmayabilir. Bu ihtimali göz önünde bulundurarak pool isimde bir tablomuz mevcut. Bu tablo ile CompanyId ilişkili olduğu için hangi firmanın hangi sunucuda veri tabanı var aslında bunu görebiliyoruz.

User: Kullanıcıları tuttuğumuz tablodur.

Ana veri tabanımız hazır fakat her bir şirket için oluşacak base veri tabanından da biraz bahsedelim.

Tekrarlayan veritabanlarında Customer ve Product adında iki adet tablomuz bulunmaktadır. İsimlerinden de anlaşılacağı üzere Müşteri ve Ürün tablolarımız mevcuttur.

Veri tabanı tarafındaki açıklamalarımızı bitirdikten sonra kod tarafına geçelim.

Solution bazında projemizin genel yapısı

Auth.API: Web API projesi olarak yapılandırdığımız bu proje diğer projelere erişmek için JWT ile token almayı sağladığımız projedir. Bu proje MainDB ile bağlantılı tek projedir. Oturum alınan şirkete göre oturum sağlar ve bu oturumun bütün projelerde görünmesini sağlar.

Tenant.API: Veri tabanlarına erişim sağlayan genel uygulamamız. Auth.API tarafından aldığımız token ile hangi şirkete bağlanıp işlem işlem yapmamızı sağlayan projedir.

Shared.Domain: Ortak modelleri tuttuğumuz proje

Shared.Infrastructure: Ortak olabilecek yardımcı sınıflar ve servislerin toplandığı proje.

Shared.AspNetCore: Web ile ilgili yardımcı method ve sınıfların bulunduğu proje

Docker ile projeyi ayağa kaldırmıştık şimdi test için bir token alalım ve akışa bir göz atalım.

Auth.API projesinde yer alan AuthController içerisinde yer alan Post methodu ile token isteği oluşturuyoruz.

IUserService ile MainDB üzerinde yer alan bilgileri sorguluyoruz bilgiler doğru ise orada oluşturduğumuz TenantInfo adındaki bir model ile geriye dönüyoruz. Bu model üzerinde yer alan bilgiler hangi kullanıcı, hangi veri tabanı, hangi sunucu gibi bir çok soruya yanıt veriyor. Bu bilgiler ile AuthService üzerinde bir token oluşturarak bu bilgileri de token üzerine ekleyerek diğer projelerde de oluşturduğumuz token içindeki bilgileri okumak istiyoruz.

Parametreler (JwtTokenConfig) appsettings.json üzerinden okunmaktadır. Aynı parametreler ConnectionString hariç Tenant.API üzerinde de mevcuttur çünkü Auth.API tarafından aldığınız oturumun Tenant.API üzerinde tanınması için JWT ayarlarının aynı olması gerekmektedir.

Genel bir bakıştan sonra ilk isteğimizi atıp bir token alalım.

Kod içerisinde swagger dosyası yer almaktadır. Dilerseniz oradaki dosyayı import ederek kullanabilirsiniz

İsteği gönderirken dikkat etmeniz gereken CompanyId parametresidir

CompanyId: Hangi şirket için token almak ister isek o şirket için Id vermeliyiz. Fakat bu Id için yetkiniz olması gerekmektedir. Bu yetkiler Company_User_Mapping tablosunda yer almaktadır aksi taktirde hata alırsınız.

Geri kalan bilgiler User tablosunda yer alan bilgilerdir. Girmiş olduğunuz şifre arka tarafta hash hale getirilerek eşleştirme yapılmaktadır.

Token aldığımıza göre ilk isteğimizi atabiliriz 😊

Product listesini almak için aldığımız token bilgisini ilgili yere ekliyoruz

İlk sonucumuzu alıyoruz. Bu veriler MultiTenantDB_2 isimli veri tabanından gelmektedir çünkü token alırken 2 numaralı şirket için istek yapmıştık.

Peki TenantAPI tarafında işler nasıl yürüyor ilgili bağlantılar nasıl gerçekleşiyor biraz bunu inceleyelim.

İlk istek gelirken JwtMiddleware tarafın düşüyor ve burada token kontrol edilerek ilgili token çözülüp gerekli olan bilgiler HttpContext üzerinden sürekli iletiliyor.

AuthService üzerinde token doğrulanıyor ve Claim tarafında yer alan TenantInfo modeli çözülüp HttpContext ile iletiliyor.

Bu bilgiler çözüldükten sonra erişmek için WorkContext adında bir yardımcı class aracılığı ile bu bilgi çözülerek projenin her noktasından erişilme imkanı sağlanıyor.

WorkContext tarafından TenantInfo çözülmek istenildiğinde IHttpContextAccessor aracılığı ile Middleware tarafında döndüğümüz HttpContextItem içinde yer alan nesneyi Deserialize ediyoruz.

Veri tabanı bağlantısı nasıl değişiyor sorunu duyar gibiyim aslında en kolay kısımlardan biride burası diyebilirim. Önemli olan bağlantı bilgisini çözmek.

Repository katmanı üzerinden istek iletildiği anda Context resolve ediliyor ve edildiği anda WorkContext nesnesini çözüyoruz. Context içerisinde artık hangi veri tabanına bağlanacağımızı biliyoruz

DbContext içerisinde OnConfiguring tarafında her istek geldiğinde ilgili Connection bilgisini TenantInfo üzerinden çözerek bağlantıyı değiştiriyoruz. Böylece kullanıcıya göre bağlantı değişerek istek gelen kullanıcı için oluşturulan veri tabanı üzerinden sorgulamayı gerçekleştiriyoruz.

Az önce yaptığımız sorgulama üzerinde companyId 2 olarak sorgulamıştık bu sefer 1 olarak sorgulayıp sonuçların değiştiğini görelim.

CompanyId 1 için token aldık

Gelen ürünlerin bu sefer MultiTenantDB_1 numaralı veri tabanından geldiğini görüyoruz.

Proje içerisinde paylaştığım swagger dosyasını import ederek diğer fonksiyonları da test edebilirsiniz.

Gösterdiğim listelem methodundan ayrı olarak Get, Insert, Update, Delete methodlarını kullanabilirsiniz

--

--