🏢 معماری چند-مستأجره (Multi-Tenancy) با EF Core (قسمت ۱):
یک دیتابیس برای همه اپلیکیشنهای مدرن SaaS (نرمافزار به عنوان سرویس) یک ویژگی مشترک دارن: چند-مستأجری (Multi-Tenancy).
یعنی یک اپلیکیشن به چندین مشتری (مستأجر یا tenant) سرویس میده، ولی دادههای هر مشتری کاملاً از بقیه ایزوله هست. امروز در قسمت اول از این سری، با این معماری و روش پیادهسازی "یک دیتابیس برای همه" با EF Core آشنا میشیم.
1️⃣ دو رویکرد اصلی برای چند-مستأجری
برای ایزوله کردن دادههای مشتریان، دو راه اصلی وجود داره:
🧠 ایزولهسازی منطقی (Logical Isolation):
یک دیتابیس واحد برای همه مشتریان، که دادهها با یک شناسه مثل TenantId از هم جدا میشن. (موضوع این پست)
🏬 ایزولهسازی فیزیکی (Physical Isolation):
یک دیتابیس کاملاً مجزا برای هر مشتری.
انتخاب بین این دو، به نیاز پروژه بستگی داره. برای صنایعی مثل حوزه سلامت که به ایزولهسازی بالا نیاز دارن، روش دوم الزامیه.
2️⃣ ابزار اصلی ما: EF Core Query Filters 🔍
برای پیادهسازی روش "یک دیتابیس برای همه"، ابزار اصلی ما در EF Core، فیلترهای کوئری سراسری (Global Query Filters) هست. این قابلیت به ما اجازه میده یه شرط WHERE رو به صورت خودکار به تمام کوئریهای یک انتیتی خاص اضافه کنیم. یک بار پیادهسازیش میکنیم و دیگه تقریباً فراموشش میکنیم!
3️⃣ پیادهسازی قدم به قدم
برای این کار به دو چیز نیاز داریم: ۱. راهی برای شناسایی مستأجر فعلی. ۲. راهی برای فیلتر کردن دادهها برای اون مستأجر.
• قدم اول: شناسایی مستأجر با TenantProvider
اول از همه، باید بفهمیم درخواست فعلی برای کدوم مستأجره. ما این منطق رو تو یه کلاس جدا به اسم TenantProvider کپسوله میکنیم. این کلاس، TenantId رو از هدر (X-TenantId) درخواست HTTP میخونه.
public sealed class TenantProvider
{
private const string TenantIdHeaderName = "X-TenantId";
private readonly IHttpContextAccessor _httpContextAccessor;
public TenantProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string TenantId => _httpContextAccessor
.HttpContext?
.Request
.Headers[TenantIdHeaderName];
}
💡نکته: راههای امنتر دیگری هم برای گرفتن TenantId وجود داره مثل JWT Claim یا API Key.
• قدم دوم: اعمال فیلتر سراسری در DbContext
حالا که میتونیم مستأجر فعلی رو پیدا کنیم، باید به EF Core بگیم که تمام کوئریها رو بر اساس اون فیلتر کنه. بهترین جا برای این کار، متد OnModelCreating در DbContext ماست.
public class OrdersDbContext : DbContext
{
private readonly string _tenantId;
public OrdersDbContext(
DbContextOptions<OrdersDbContext> options,
TenantProvider tenantProvider)
: base(options)
{
// TenantId رو از TenantProvider میگیریم
_tenantId = tenantProvider.TenantId;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// این فیلتر به صورت خودکار به تمام کوئریهای انتیتی Order اضافه میشه
modelBuilder
.Entity<Order>()
.HasQueryFilter(o => o.TenantId == _tenantId);
}
}
حالا هر کوئریای که روی جدول Order زده بشه، EF Core به صورت خودکار شرط WHERE TenantId = 'current_tenant_id' رو بهش اضافه میکنه!
🔖 هشتگها:
#CSharp #ASPNETCore #DotNet #MultiTenancy #EntityFrameworkCore #SoftwareArchitecture #Backend
🏢 معماری چند-مستأجره با EF Core (قسمت ۲):
هر مستأجر، دیتابیس خودش
در قسمت اول، روش ایزولهسازی منطقی (یک دیتابیس برای همه) رو با Query Filters پیادهسازی کردیم. این روش برای خیلی از پروژهها عالیه.
اما گاهی اوقات به ایزولهسازی فیزیکی نیاز داریم؛ یعنی هر مشتری، دیتابیس کاملاً مجزای خودش رو داشته باشه تا دادهها به صورت فیزیکی از هم جدا باشن. امروز با این روش پیشرفته و امن آشنا میشیم.
1️⃣ چالش جدید: مدیریت Connection String داینامیک 🔌
در این سناریو، دیگه Query Filters به کارمون نمیاد، چون با دیتابیسهای مختلفی سر و کار داریم. چالش اصلی ما اینه که به ازای هر درخواست، بتونیم DbContext رو با Connection String مخصوص همون مستأجر پیکربندی کنیم.
2️⃣ پیادهسازی قدم به قدم
قدم اول: ذخیره اطلاعات مستأجرها ⚙️
اول از همه، باید اطلاعات هر مستأجر و Connection String دیتابیسش رو یه جا ذخیره کنیم. برای مثال، در فایل appsettings.json:
"Tenants": [
{ "Id": "tenant-1", "ConnectionString": "Host=tenant1.db;Database=db1" },
{ "Id": "tenant-2", "ConnectionString": "Host=tenant2.db;Database=db2" }
]
قدم دوم: آپدیت کردن TenantProvider 🕵️♂️حالا TenantProvider رو آپدیت میکنیم تا علاوه بر TenantId، بتونه Connection String مربوط به مستأجر فعلی رو هم از تنظیمات بخونه و به ما بده.
public sealed class TenantProvider
{
// ... (Properties for HttpContextAccessor and TenantSettings)
public TenantProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<TenantSettings> tenantsOptions)
{
// ...
_tenantSettings = tenantsOptions.Value;
}
public string TenantId => /* ... gets tenantId from header ... */;
public string GetConnectionString()
{
return _tenantSettings.Tenants
.Single(t => t.Id == TenantId).ConnectionString;
}
}
قدم سوم: جادوی DI (ثبت داینامیک DbContext) ✨این مهمترین و جادوییترین بخش کاره! ما DbContext رو جوری در Program.cs ثبت میکنیم که به ازای هر درخواست، اول TenantProvider رو اجرا کنه، Connection String رو بگیره و بعد DbContext رو با اون پیکربندی کنه.
builder.Services.AddDbContext<OrdersDbContext>((serviceProvider, options) =>
{
// TenantProvider رو از DI میگیریم
var tenantProvider = serviceProvider.GetRequiredService<TenantProvider>();
// Connection String مخصوص مستأجر فعلی رو میگیریم
var connectionString = tenantProvider.GetConnectionString();
// و DbContext رو با اون کانفیگ میکنیم
options.UseSqlServer(connectionString);
});
🔐 نکته امنیتی مهم
قرار دادن Connection Stringها به صورت مستقیم در appsettings.json برای محیط پروداکشن امن نیست. همیشه از ابزارهای مدیریت secret مثل Azure Key Vault یا NET User Secrets. برای محیط توسعه استفاده کنید.
🤔 حرف حساب و تجربه شمابا این دو روش
شما الان جعبه ابزار کاملی برای پیادهسازی هر نوع معماری چند-مستأجره در ASP.NET Core دارید.
شما تو پروژههاتون با کدوم مدل چند-مستأجری کار کردید؟ تک دیتابیس یا چند دیتابیس؟ چالشها و مزایای هر کدوم از نظر شما چیه؟
🔖 هشتگها:
#CSharp #ASPNETCore #DotNet #MultiTenancy #EntityFrameworkCore #SoftwareArchitecture #Backend