C# Geeks (.NET) – Telegram
می‌تونید مشکل این کد رو پیدا کنید؟ 🧐

پیدا کردنش یه چیزه. چطور حلش می‌کنید؟

😈اینجا نقطه‌ایه که همه چیز خراب میشه:
شما یک کاربر رو در دیتابیس ذخیره می‌کنید.
بعد یه ایمیل خوش‌آمدگویی می‌فرستید.
و بعد یه پیام رو برای یه سرویس دیگه منتشر می‌کنید.
این‌ها سه تا عملیات مجزا هستن و هر کدومشون ممکنه شکست بخوره.
حالا سیستم شما در یک وضعیت عجیب و ناسازگار (inconsistent) قرار می‌گیره:
• کاربر ذخیره شده، ولی ایمیل هرگز ارسال نشده.
• یا پیام در صف قرار نگرفته.
• یا عملیات رو دوباره تکرار کردید و همه چیز دو بار انجام شده!
وضعیت به سرعت بهم‌ریخته و پیچیده میشه.

راه حل: الگوی Outbox 📬

الگوی Outbox به شما کمک می‌کنه از این مشکل جلوگیری کنید.
این الگو اینجوری کار می‌کنه:
1️⃣ یک تراکنش (transaction) دیتابیس رو شروع کنید.
2️⃣ کاربر رو ذخیره کنید.
3️⃣ قصدِ ارسال ایمیل و انتشار پیام رو در یک جدول به نام Outbox (صندوق خروجی) درج کنید.
4️⃣ تراکنش رو Commit کنید.
بعداً، یک سرویس پس‌زمینه (background service) این رکوردهای Outbox رو برمی‌داره و اون‌ها رو به صورت قابل اطمینان و با مکانیزم تلاش مجدد (retry) پردازش می‌کنه.

✨️ نتیجه نهایی:
حالا، تمام این مراحل با هم موفق میشن، یا هیچکدومشون انجام نمیشن.
عملیات نوشتن در دیتابیس شما اتمیک (atomic) هست. دلیلی نداره که جریان‌های کاری شما هم اتمیک نباشن.
🛠 متدهای مدرن در #C
Expression-bodied و Local Methods

سی‌شارپ مدرن، پر از قابلیت‌های شیک و کاربردیه که به ما کمک می‌کنه کدهای کوتاه‌تر، خواناتر و منظم‌تری بنویسیم. امروز با دو تا از این تکنیک‌های خفن برای کار با متدها آشنا میشیم.

1️⃣ متدهای Expression-bodied (=>): خداحافظی با return و {}

اگه متد شما فقط از یک عبارت (Expression) تشکیل شده، دیگه نیازی به نوشتن بلوک {} و کلمه کلیدی return ندارید! می‌تونید با استفاده از "فت ارو" (=>)، اون رو در یک خط بنویسید.

این کار، کد شما رو فوق‌العاده تمیز و مینیمال می‌کنه.
// روش قدیمی
int Double(int x)
{
return x * 2;
}

// روش مدرن و شیک با Expression-bodied
int DoubleModern(int x) => x * 2;

// این قابلیت برای متدهای void هم کار می‌کنه
void SayHello() => Console.WriteLine("Hello!");


2️⃣ متدهای محلی (Local Methods): متد در دل متد!

گاهی وقتا یه منطق کمکی دارید که فقط و فقط داخل یک متد دیگه استفاده میشه. به جای اینکه اون رو به عنوان یه متد private تو کل کلاس تعریف کنید و کلاس رو شلوغ کنید، می‌تونید اون رو به عنوان یه متد محلی، دقیقاً داخل همون متدی که بهش نیاز دارید، تعریف کنید.

مزایای این کار:

• کپسوله‌سازی عالی: اون متد کمکی، فقط همونجا قابل دسترسه و هیچ جای دیگه‌ای از کلاس رو آلوده نمی‌کنه.

• دسترسی به متغیرهای محلی: متد محلی می‌تونه به متغیرهای محلی و پارامترهای متد بیرونی دسترسی داشته باشه.
void WriteCubes()
{
Console.WriteLine(Cube(3));
Console.WriteLine(Cube(4));

// این متد، فقط داخل WriteCubes قابل دسترسه
int Cube(int value) => value * value * value;
}


💡نکته حرفه‌ای (static local methods):
از C# 8 به بعد، اگه متد محلی شما نیازی به دسترسی به متغیرهای متد بیرونی نداره، می‌تونید اون رو با کلمه کلیدی static تعریف کنید. این کار به کامپایلر کمک می‌کنه بهینه‌سازی‌های بهتری انجام بده و جلوی دسترسی‌های ناخواسته رو هم می‌گیره.

🤔 حرف حساب و تجربه شما

این تکنیک‌های مدرن، ابزارهای شما برای نوشتن کدهایی هستن که هم کار می‌کنن و هم خوندنشون لذت‌بخشه.

شما از کدوم یکی از این قابلیت‌ها بیشتر استفاده می‌کنید؟ آیا متدهای Expression-bodied جزو استایل کدنویسی شما هستن؟

🔖 هشتگ‌ها:
#CSharp #Programming #Developer #DotNet #CleanCode #ModernCSharp #BestPractices
🏛️ مونولیت ماژولار چیست؟ معماری هوشمندانه‌ای که قبل از میکروسرویس باید بشناسید
همیشه بحث بوده: سادگی و یکپارچگی مونولیت یا انعطاف‌پذیری و قدرت میکروسرویس؟ اما اگه یه راه سومی وجود داشته باشه که بهترین مزایای هر دو رو داشته باشه چی؟
با معماری مونولیت ماژولار (Modular Monolith) آشنا بشید؛ رویکردی که سادگی مونولیت رو با نظم و مرزبندی میکروسرویس ترکیب می‌کنه.

1️⃣ مونولیت ماژولار به زبان ساده چیه؟ 🧱
تصور کنید به جای ساختن یه ساختمون غول‌پیکر یک‌تکه (مونولیت سنتی)، یا چندین ساختمون کاملاً جدا از هم (میکروسرویس)، شما یک مجتمع آپارتمانی بزرگ می‌سازید.
کل مجتمع یک واحد یکپارچه است، ولی داخلش به واحدهای مستقل و ایزوله با دیوارهای محکم تقسیم شده. این واحدها همون ماژول‌ها هستن (مثلاً ماژول پرداخت، ماژول کاربران). هر ماژول کارکردهای مرتبط به خودش رو گروه‌بندی می‌کنه و مرزهای مشخصی داره.
💡نکته کلیدی: این واحدها (ماژول‌ها) اتصال سستی (loosely coupled) با هم دارن و فقط از طریق یک API عمومی و مشخص با هم صحبت می‌کنن، نه اینکه در جزئیات پیاده‌سازی هم دخالت کنن.


انعطاف‌پذیری در عمل: فرض کنید در فصل تعطیلات، ماژول رزرو شما نیاز به منابع بیشتری داره. در این معماری، شما می‌تونید موقتاً همون ماژول رو به صورت مستقل دیپلوی کنید و بعداً دوباره به مونولیت اصلی برش گردونید!
2️⃣ چرا "Monolith First"؟ (حتی به گفته بزرگان!) 💡
با اینکه میکروسرویس‌ها خیلی محبوبن، پیچیدگی‌های سیستم‌های توزیع‌شده (Distributed Systems) رو به همراه دارن. برای همین، خیلی از متخصصان، از جمله مارتین فاولر، میگن:

"شما نباید یک پروژه جدید را با میکروسرویس‌ها شروع کنید، حتی اگر مطمئن باشید که اپلیکیشن شما به اندازه‌ای بزرگ خواهد شد که این کار ارزشش را داشته باشد."
حتی گوگل هم در مقالات تحقیقاتی جدیدش، به ترند مونولیت ماژولار پیوسته. دلیل این امر، چالش‌های ذاتی میکروسرویس‌هاست:
1️⃣ پرفورمنس: سربار شبکه و سریال‌سازی داده‌ها، عملکرد رو کاهش میده.
2️⃣ صحت: تضمین صحت یک سیستم توزیع‌شده بسیار دشواره.
3️⃣ مدیریت: شما باید چندین اپلیکیشن مختلف با چرخه‌های انتشار متفاوت رو مدیریت کنید.
4️⃣ بعدی APIهای منجمد: تغییر APIها بدون شکستن سرویس‌های دیگه، سخت میشه.
5️⃣ سرعت توسعه: یک تغییر کوچک در یک سرویس، ممکنه نیازمند تغییر و هماهنگی در چندین سرویس دیگه باشه.

حالا که فهمیدیم مونولیت ماژولار چیه و چرا یک انتخاب هوشمندانه‌ست، در قسمت بعدی این سری به صورت عمیق به مزایای مشخص اون می‌پردازیم و اون رو مستقیماً با میکروسرویس‌ها مقایسه می‌کنیم.

🔖 هشتگ‌ها:
#SoftwareArchitecture #CSharp #DotNet #Monolith #Microservices #Developer #CleanCode
⚖️ دوئل معماری: مزایای مونولیت ماژولار در برابر میکروسرویس‌ها

حالا بیاید ببینیم این معماری در عمل چه مزایای ملموسی داره و تفاوت اصلیش با میکروسرویس‌ها چیه.

1️⃣ مهم‌ترین مزایای مونولیت ماژولار

🚀 استقرار ساده‌شده:
برخلاف میکروسرویس‌ها که نیازمند زیرساخت و استراتژی‌های پیچیده هستن، یک مونولیت ماژولار به سادگی و به عنوان یک واحد دیپلوی میشه.

⚡️ پرفورمنس بالا:
ارتباط بین ماژول‌ها به صورت درون-فرآیندی (in-process) و مستقیمه. این یعنی هیچ تأخیر شبکه یا سربار سریال‌سازی داده‌ای که در میکروسرویس‌ها وجود داره، اینجا نیست.

💻 سرعت توسعه بالا:
شما با یک پایگاه کد واحد سر و کار دارید. این کار دیباگ کردن، تست و تجربه کلی توسعه رو به شدت ساده و سریع می‌کنه.

🔗 مدیریت تراکنش آسان:
مدیریت تراکنش‌ها در سیستم‌های توزیع‌شده یه کابوس واقعیه. اما در مونولیت ماژولار، چون ماژول‌ها می‌تونن از یک دیتابیس مشترک استفاده کنن، این کار خیلی ساده‌تره.

🛠️ پیچیدگی عملیاتی کمتر:
شما فقط یک اپلیکیشن رو مدیریت، مانیتور و دیپلوی می‌کنید، نه ده‌ها سرویس مختلف رو.

🌱 مسیر هموار به سمت میکروسرویس:
اگه در آینده نیاز شد، به راحتی می‌تونید یه ماژول رو از ساختار اصلی جدا کنید و به یه سرویس مستقل تبدیلش کنید! این بزرگترین مزیت بلندمدت این معماریه.

2️⃣ مقایسه نهایی: مونولیت ماژولار در برابر میکروسرویس‌ها

بزرگترین تفاوت در نحوه استقرار (Deployment) اون‌هاست. میکروسرویس‌ها مرزهای منطقی داخل یه مونولیت ماژولار رو به مرزهای فیزیکی ارتقا میدن.
• مونولیت ماژولار به شما میده:
انسجام بالا، اتصال سست، کپسوله‌سازی داده و تمرکز بر کارکردهای بیزینسی.

•میکروسرویس‌ها به شما میده:
تمام موارد بالا + دیپلوی مستقل، مقیاس‌پذیری مستقل و توانایی استفاده از پشته‌های فناوری (tech stacks) مختلف برای هر سرویس.

3️⃣حرف آخر: نقل قول طلایی 🎯

همونطور که سایمون براون میگه:
"میکروسرویس‌ها را به خاطر مزایایشان انتخاب کنید، نه به این دلیل که پایگاه کد مونولیت شما یک فاجعه است."


مونولیت ماژولار به شما اجازه میده ابتدا خانه‌ی خود را مرتب کنید و بعداً تصمیم بگیرید که آیا واقعاً به چندین خانه جداگانه نیاز دارید یا نه.
🤔 نظر شما چیه؟
شما تو پروژه‌هاتون تجربه کار با مونولیت ماژولار رو داشتید؟ به نظرتون بزرگترین مزیت یا چالش این معماری چیه؟[منبع]

🔖 هشتگ‌ها:
#SoftwareArchitecture #CSharp #DotNet #Monolith #Microservices #Developer #CleanCode
🏗 سازنده‌ها (Constructors) در #C: معمار آبجکت‌های شما

هر آبجکتی در #C یه "لحظه تولد" داره. اون لحظه، کدی اجرا میشه که آبجکت رو برای زندگی آماده می‌کنه. به این کد جادویی میگن سازنده (Constructor).

سازنده، اولین متدیه که موقع ساختن یک نمونه جدید از کلاس با کلمه کلیدی new اجرا میشه. امروز می‌خوایم با تمام زیر و بم این معماران آبجکت آشنا بشیم.

1️⃣ سازنده چیست و چطور کار می‌کند؟

سازنده شبیه یک متده، با این تفاوت که اسمش دقیقاً هم‌اسم خود کلاس هست و هیچ نوع خروجی‌ای (حتی void) نداره. کار اصلیش، مقداردهی اولیه فیلدها و آماده‌سازی آبجکته.
public class Panda
{
string _name; // فیلد

// این سازنده است
public Panda(string name)
{
_name = name; // کد مقداردهی اولیه
}
}

// ... نحوه استفاده
Panda p = new Panda("Petey"); // سازنده اینجا صدا زده میشه


💡نکته حرفه‌ای: برای سازنده‌های تک‌خطی، می‌تونید از سینتکس Expression-bodied استفاده کنید:
public Panda(string name) => _name = name;
ترفند this: اگه اسم پارامتر با اسم فیلد یکی بود، می‌تونید با کلمه کلیدی this به فیلد کلاس اشاره کنید:
public Panda(string name) => this.name = name;

2️⃣ اورلودینگ و زنجیره‌ای کردن سازنده‌ها (this):

شما می‌تونید چندین سازنده با ورودی‌های مختلف (Overloading) داشته باشید. برای جلوگیری از تکرار کد، یک سازنده می‌تونه اون یکی رو با کلمه کلیدی : this(...) صدا بزنه.
public class Wine
{
public decimal Price;
public int Year;

public Wine(decimal price)
{
Price = price;
}

// این سازنده، قبل از اجرای بدنه خودش، سازنده بالایی رو صدا میزنه
public Wine(decimal price, int year) : this(price)
{
Year = year;
}
}


3️⃣ تله‌ی سازنده پیش‌فرض! ⚠️

قانون مهم: کامپایلر #C فقط و فقط زمانی که شما هیچ سازنده‌ای در کلاس تعریف نکرده باشید، یه سازنده عمومی بدون پارامتر ( ()public MyClass) براتون میسازه.

به محض اینکه شما حتی یک سازنده (با یا بدون پارامتر) بنویسید، اون سازنده اتوماتیک حذف میشه و اگه بهش نیاز دارید، باید خودتون دستی بنویسیدش. این یکی از خطاهای رایج برای تازه‌کارهاست!

4️⃣ ترتیب اجرا: اول فیلدها، بعد سازنده

اگه فیلدهاتون مقدار اولیه دارن، این مقداردهی همیشه قبل از اجرای اولین خط کد در سازنده و به ترتیب تعریفشون در کلاس انجام میشه.
class Player
{
int shields = 50; // این اول اجرا میشه
int health = 100; // این دوم اجرا میشه

public Player()
{
// این سوم اجرا میشه
Console.WriteLine("Constructor executed!");
}
}


5️⃣ کاربرد حرفه‌ای: سازنده‌های private

چرا باید یه سازنده رو private کنیم؟ برای اینکه کنترل کامل ساخت آبجکت رو به دست بگیریم! این الگو (که بهش Factory Method میگن) به ما اجازه میده که منطق ساخت رو داخل یه متد استاتیک قرار بدیم. مثلا برای پیاده‌سازی الگوی Singleton یا برگردوندن آبجکت از یک Pool.
public class MyApiClien
{
// سازنده خصوصیه، پس کسی از بیرون نمی‌تونه new کنه
private MyApiClient() { }

// فقط از طریق این متد استاتیک میشه نمونه ساخت
public static MyApiClient Create()
{
// ... منطق پیچیده برای ساخت و کانفیگ ...
return new MyApiClient();
}
}

🔖 هشتگ‌ها:
#CSharp #Programming #Developer #DotNet #OOP #Constructor #BestPractices
🚀 کشینگ در ASP.NET Core (قسمت ۱):

مبانی و چرا باید اهمیت بدیم؟
یکی از ساده‌ترین و در عین حال قدرتمندترین تکنیک‌ها برای افزایش چشمگیر پرفورمنس اپلیکیشن شما، کشینگ (Caching) هست.

کشینگ یعنی ذخیره موقت داده‌ها در یک محل سریع‌تر (معمولاً حافظه RAM). شما معمولاً نتایج عملیات‌های سنگین یا داده‌هایی که به صورت مکرر بهشون نیاز دارید رو کش می‌کنید تا در درخواست‌های بعدی، به جای مراجعه به منبع اصلی (مثل دیتابیس)، مستقیماً از کش استفاده کنید.

امروز قسمت اول از مینی-سریال جدیدمون رو شروع می‌کنیم و به مبانی این موضوع حیاتی می‌پردازیم.

1️⃣ کشینگ چطور پرفورمنس رو بهبود میده؟ ⚡️

🚀 دسترسی سریع‌تر به داده‌ها:

داده‌های کش شده از حافظه RAM خوانده میشن که هزاران برابر سریع‌تر از دیتابیس یا یک API خارجیه.

📉 کاهش بار دیتابیس:

با کش کردن داده‌هایی که زیاد خونده میشن، تعداد کوئری‌ها به دیتابیس به شدت کم میشه و فشار از روی دیتابیس برداشته میشه.

🧠 استفاده کمتر از CPU:

جلوی پردازش‌های تکراری و سنگین (مثل ساختن یک صفحه وب پیچیده) رو می‌گیره.

💪 مقیاس‌پذیری بیشتر:

برنامه شما می‌تونه ترافیک بیشتری رو با منابع کمتر مدیریت کنه و به کاربران بیشتری سرویس بده.

2️⃣ ابزارهای کشینگ در ASP.NET Core 🛠

حالا ASP.NET Core دو تا ابزار (اینترفیس) اصلی برای کار با کش در اختیار ما میذاره:

• IMemoryCache:

برای کشینگ درون-حافظه‌ای (In-Memory). داده‌ها رو در حافظه RAM همون سروری که اپلیکیشن روش اجرا شده، ذخیره می‌کنه. استفاده ازش خیلی ساده‌ست ولی برای سناریوهایی که چند سرور دارید، مناسب نیست.

• IDistributedCache:

برای کشینگ توزیع‌شده (Distributed). به شما اجازه میده داده‌های کش رو در یک سرور خارجی مثل Redis ذخیره کنید تا بین چندین نمونه (instance) از اپلیکیشن شما به اشتراک گذاشته بشه.

🔖 هشتگ‌ها:
#ASPNETCore #Caching
🚀 کشینگ در ASP.NET Core (قسمت ۲):

شیرجه عمیق در IMemoryCache
در قسمت اول با مبانی کشینگ آشنا شدیم. حالا وقتشه آستین‌ها رو بالا بزنیم و به صورت عملی، اولین و ساده‌ترین نوع کشینگ در ASP.NET Core یعنی کشینگ درون-حافظه‌ای (In-Memory) رو با IMemoryCache پیاده‌سازی کنیم.

1️⃣ قدم اول: فعال‌سازی سرویس

مثل خیلی از قابلیت‌های دیگه در ASP.NET Core، اول باید سرویس IMemoryCache رو در فایل Program.cs به اپلیکیشن خودمون اضافه کنیم تا بتونیم اون رو از طریق Dependency Injection (تزریق وابستگی) همه جا استفاده کنیم.
var builder = WebApplication.CreateBuilder(args);

// اضافه کردن سرویس کشینگ درون-حافظه‌ای
builder.Services.AddMemoryCache();

// ... بقیه تنظیمات


2️⃣ قدم دوم: الگوی پیاده‌سازی (Try-Get-Set)

الگوی استاندارد برای کار با کش خیلی ساده‌ست:

• امتحان کن (Try): سعی کن داده رو از کش بگیری.

• بگیر (Get): اگه تو کش بود، همونو برگردون.

• تنظیم کن (Set): اگه تو کش نبود، از منبع اصلی (مثل دیتابیس) بگیر و برای دفعه‌های بعدی، تو کش ذخیره کن.

این الگو رو در یک Minimal API ببینیم:
app.MapGet("products/{id}", 
(int id, IMemoryCache cache, AppDbContext context) =>
{
// ۱. سعی می‌کنیم محصول رو از کش با کلید id بخونیم
if (!cache.TryGetValue(id, out Product product))
{
// ۲. اگه تو کش نبود (Cache Miss)، از دیتابیس می‌خونیم
product = context.Products.Find(id);

// ۳. داده رو در کش ذخیره می‌کنیم تا دفعه بعد استفاده بشه
// (گزینه‌های انقضا رو در بخش بعدی توضیح میدیم)
var cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5));

cache.Set(id, product, cacheOptions);
}

// ۴. داده رو (چه از کش، چه از دیتابیس) برمی‌گردونیم
return Results.Ok(product);
});


3️⃣ قدم سوم: مدیریت انقضای کش (Cache Expiration) ⏱️

داده‌ها نباید برای همیشه تو کش بمونن، چون ممکنه در دیتابیس تغییر کنن و کهنه (stale) بشن. ما باید به کش بگیم که چه زمانی این داده‌ها رو دور بریزه. دو تا سیاست اصلی برای این کار وجود داره:

✨️انقضای مطلق (Absolute Expiration):
یه تاریخ انقضای مشخص تعیین می‌کنه. مثلاً "۱۰ دقیقه دیگه، چه کسی از این داده استفاده کرد یا نکرد، حذفش کن."
.SetAbsoluteExpiration(TimeSpan.FromMinutes(10))

✨️انقضای لغزنده (Sliding Expiration):
یه بازه زمانی عدم فعالیت تعیین می‌کنه. مثلاً "اگه تا ۲ دقیقه کسی به این داده دست نزد، حذفش کن. ولی اگه کسی ازش استفاده کرد، عمرش رو ۲ دقیقه دیگه تمدید کن."
.SetSlidingExpiration(TimeSpan.FromMinutes(2))

🔖 هشتگ‌ها:
#ASPNETCore #Caching
🚀 کشینگ در ASP.NET Core (قسمت ۳):

الگوی حرفه‌ای Cache-Aside
در قسمت قبل، با IMemoryCache آشنا شدیم. عالی بود، ولی یه مشکل بزرگ داشت: اگه چند تا سرور داشته باشیم، کش بینشون به اشتراک گذاشته نمیشه.

امروز می‌خوایم با الگوی Cache-Aside و اینترفیس IDistributedCache آشنا بشیم تا این مشکل رو حل کنیم و کدهامون رو برای کشینگ، خیلی تمیزتر و قابل استفاده مجدد کنیم.

1️⃣ الگوی Cache-Aside چیست؟ 🤓

این الگو، رایج‌ترین و استانداردترین استراتژی برای کار با کشه. منطقش خیلی ساده‌ست:

• اول کش رو چک کن: برنامه شما اول به کش نگاه می‌کنه.

• اگه تو کش بود، برش گردون: اگه داده اونجا بود (Cache Hit)، کار تمومه.

• اگه نبود، برو سراغ منبع اصلی: اگه داده تو کش نبود (Cache Miss)، برو از منبع اصلی (مثل دیتابیس) بخونش.

• کش رو آپدیت کن و برگردون: داده‌ای که از منبع اصلی گرفتی رو تو کش ذخیره کن تا برای دفعه بعد آماده باشه و بعد به کاربر برگردون.

2️⃣ ساخت یک ابزار حرفه‌ای: متد توسعه GetOrCreateAsync 🛠

به جای اینکه این منطق ۴ مرحله‌ای رو هر بار تکرار کنیم، می‌تونیم یه متد توسعه (Extension Method) خفن برای IDistributedCache بنویسیم که این کار رو برامون انجام بده.
public static class DistributedCacheExtensions
{
// یه زمان انقضای پیش‌فرض تعریف می‌کنیم
public static DistributedCacheEntryOptions DefaultExpiration => new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
};

public static async Task<T> GetOrCreateAsync<T>(
this IDistributedCache cache,
string key,
Func<Task<T>> factory, // تابعی که قراره داده رو از منبع اصلی بگیره
DistributedCacheEntryOptions? options = null)
{
var cachedData = await cache.GetStringAsync(key);
if (cachedData is not null)
{
// اگه داده تو کش بود، از JSON برش می‌گردونیم
return JsonSerializer.Deserialize<T>(cachedData);
}

// اگه نبود، تابع factory رو اجرا می‌کنیم تا از منبع اصلی بگیره
var data = await factory();

// داده جدید رو به صورت JSON در کش ذخیره می‌کنیم
await cache.SetStringAsync(
key,
JsonSerializer.Serialize(data),
options ?? DefaultExpiration);

return data;
}
}


3️⃣ نحوه استفاده از ابزار جدید

حالا ببینید اون کد شلوغ قبلی، با این متد توسعه چقدر تمیز و خوانا میشه:
app.MapGet("products/{id}", 
async (int id, IDistributedCache cache, AppDbContext context) =>
{
// فقط کافیه متد خودمون رو صدا بزنیم!
var product = await cache.GetOrCreateAsync($"products-{id}", async () =>
{
// این تابع فقط زمانی اجرا میشه که داده تو کش نباشه
return await context.Products.FindAsync(id);
});

return Results.Ok(product);
});


💡نکته: برای اینکه این کد کار کنه، باید اول سرویس IDistributedCache رو در Program.cs ثبت کنیم:
builder.Services.AddDistributedMemoryCache();

🔖 هشتگ‌ها:
#ASPNETCore #Caching
🚀 کشینگ در ASP.NET Core (قسمت ۴):

قدرت توزیع‌شده با Redis
در قسمت قبل، یه متد توسعه خفن برای IDistributedCache نوشتیم. اما پیاده‌سازی پیش‌فرض اون (AddDistributedMemoryCache)، هنوز هم درون-حافظه‌ای بود و کش رو بین سرورها به اشتراک نمیذاشت.

امروز وقتشه این مشکل رو برای همیشه حل کنیم و با Redis، یکی از محبوب‌ترین و قدرتمندترین ابزارهای کشینگ توزیع‌شده، آشنا بشیم.

ردیس (Redis) چیست؟ 🧠

ردیس یک ذخیره‌ساز داده درون-حافظه‌ای (in-memory) فوق‌العاده سریعه که اغلب به عنوان یک کش توزیع‌شده با پرفورمنس بالا استفاده میشه. وقتی از Redis به عنوان کش استفاده می‌کنید، تمام نمونه‌های (instances) اپلیکیشن شما به یک سرور Redis مشترک وصل میشن و داده‌های کش رو از اونجا می‌خونن و می‌نویسن.

این یعنی اگه ۱۰ تا سرور داشته باشید، کش بین همه‌شون یکسان و هماهنگه!

1️⃣ قدم اول: نصب پکیج

برای اینکه ASP.NET Core بتونه با Redis صحبت کنه، باید پکیج مخصوصش رو نصب کنیم:

Install-Package Microsoft.Extensions.Caching.StackExchangeRedis

این پکیج به ما اجازه میده Redis رو به راحتی به عنوان پیاده‌سازی IDistributedCache به پروژه‌مون اضافه کنیم.

2️⃣ قدم دوم: پیکربندی در Program.cs

حالا باید به برنامه‌مون بگیم که از Redis استفاده کنه. دو تا راه رایج برای این کار وجود داره:

👍🏻روش ساده (با Connection String):
این روش برای شروع عالیه. فقط کافیه آدرس سرور Redis رو بهش بدید.
var builder = WebApplication.CreateBuilder(args);

string redisConnectionString = builder.Configuration.GetConnectionString("Redis");

builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConnectionString;
});


💯روش حرفه‌ای‌تر (با IConnectionMultiplexer):
این روش کنترل بیشتری به شما میده و بهترین راه برای پروژه‌های بزرگه. شما خودتون یک نمونه از ConnectionMultiplexer رو به صورت Singleton ثبت می‌کنید.
string redisConnectionString = builder.Configuration.GetConnectionString("Redis");

IConnectionMultiplexer connectionMultiplexer =
ConnectionMultiplexer.Connect(redisConnectionString);

builder.Services.AddSingleton(connectionMultiplexer);

builder.Services.AddStackExchangeRedisCache(options =>
{
options.ConnectionMultiplexerFactory =
() => Task.FromResult(connectionMultiplexer);
});

جادو اتفاق افتاد!
تمام شد! حالا هرجایی از کدتون که IDistributedCache رو تزریق کنید، در پشت صحنه به جای کش حافظه، از Redis استفاده خواهد شد، بدون اینکه نیاز به تغییر حتی یک خط از کدهای قبلی‌تون (مثل متد GetOrCreateAsync) داشته باشید. این قدرت انتزاع (Abstraction) در #C هست!

🔖 هشتگ‌ها:
#ASPNETCore #Caching #Redis
🚀 کشینگ در ASP.NET Core (قسمت ۵):

مشکل Cache Stampede و آینده کشینگ در 9 Net.

تا اینجا با انواع کشینگ آشنا شدیم. اما وقتی ترافیک سیستم خیلی بالا میره، یه مشکل خطرناک و پنهان به اسم Cache Stampede (ازدحام کش) می‌تونه تمام زحمات ما رو به باد بده!

در قسمت آخر این سری، با این مشکل و راه حل‌های مدرنش آشنا میشیم.

1️⃣ مشکل Cache Stampede چیست؟ 🔥

تصور کنید یه آیتم خیلی پرطرفدار (مثلاً صفحه اول سایت) در کش شما منقضی میشه. در یک لحظه، صدها یا هزاران درخواست همزمان می‌بینن که کش خالیه و همه‌شون با هم به سمت دیتابیس هجوم میارن تا اون داده رو بگیرن!

این هجوم ناگهانی، دیتابیس و اپلیکیشن شما رو از پا در میاره و عملاً مزیت کشینگ رو از بین می‌بره.

2️⃣ یک راه حل (ناقص): قفل‌گذاری با SemaphoreSlim 🔒


یک راه حل رایج، استفاده از قفل‌گذاری (Locking) هست. یعنی فقط به اولین درخواست اجازه بدیم که بره داده رو از دیتابیس بگیره و بقیه منتظر بمونن تا کش دوباره پر بشه.

می‌تونیم متد GetOrCreateAsync خودمون رو با SemaphoreSlim به این شکل تغییر بدیم:
public static class DistributedCacheExtensions
{
private static readonly SemaphoreSlim Semaphore = new(1, 1);

public static async Task<T> GetOrCreateAsync<T>(
this IDistributedCache cache, string key, Func<Task<T>> factory)
{
var cachedData = await cache.GetStringAsync(key);
if (cachedData is not null) return JsonSerializer.Deserialize<T>(cachedData);

try
{
await Semaphore.WaitAsync(); // منتظر قفل بمون

// دوباره کش رو چک کن، شاید درخواست قبلی پرش کرده باشه
cachedData = await cache.GetStringAsync(key);
if (cachedData is not null) return JsonSerializer.Deserialize<T>(cachedData);

// اگه هنوز خالی بود، از دیتابیس بگیر و کش رو پر کن
var data = await factory();
await cache.SetStringAsync(key, JsonSerializer.Serialize(data), ...);
return data;
}
finally
{
Semaphore.Release(); // قفل رو آزاد کن
}
}
}


🚫مشکل این راه حل: این کد از یه قفل سراسری برای تمام کلیدها استفاده می‌کنه. یعنی اگه ۱۰۰ تا درخواست برای ۱۰۰ تا کلید مختلف هم بیان، باز هم باید برای هم صبر کنن! این کارایی رو به شدت کم می‌کنه. (راه حل بهتر، قفل‌گذاری بر اساس key هست که پیچیده‌تره).

3️⃣ آینده کشینگ: HybridCache در 9 Net.

تیم دات‌نت برای حل این مشکلات (و مشکلات دیگه)، در 9 Net. یه ابزار جدید و خیلی قدرتمند به اسم HybridCache معرفی کرده.

این ابزار به صورت داخلی، مشکل Cache Stampede رو به روشی بهینه حل می‌کنه و ترکیبی از IMemoryCache (برای سرعت) و IDistributedCache (برای توزیع‌شدگی) رو به بهترین شکل ممکن ارائه میده.

جمع‌بندی سری و نظر شما 🤔
با این پست، مینی-سریال ما در مورد کشینگ به پایان میرسه. ما از مبانی شروع کردیم و به پیشرفته‌ترین مشکلات و جدیدترین راه حل‌ها رسیدیم.

🔖 هشتگ‌ها:
#ASPNETCore #Caching
ساخت آبجکت مثل یک حرفه‌ای: قدرت Object Initializers در #C


یادتونه قدیما برای ساختن و مقداردهی یه آبجکت، باید چند خط کد پشت سر هم می‌نوشتیم؟ این روش هم طولانیه و هم ممکنه باعث بشه مقداردهی بعضی پراپرتی‌ها رو فراموش کنیم.

سی‌شارپ یه راه حل خیلی شیک، مدرن و امن برای این کار داره: Object Initializers.

1️⃣ روش سنتی در برابر روش مدرن
فرض کنید این کلاس Bunny رو داریم:
public class Bunny
{
public string Name;
public bool LikesCarrots;
public bool LikesHumans;

public Bunny() {}
public Bunny(string n) => Name = n;
}

روش قدیمی و چند خطی: 👎
Bunny b1 = new Bunny();
b1.Name = "Bo";
b1.LikesCarrots = true;
b1.LikesHumans = false;

روش مدرن با Object Initializer: 👍
حالا با سینتکس {}، می‌تونیم تمام این کارها رو در یک دستور و به صورت خیلی خوانا انجام بدیم:
Bunny b2 = new Bunny 
{
Name = "Bo",
LikesCarrots = true,
LikesHumans = false
};


این قابلیت حتی با سازنده‌هایی که پارامتر دارن هم کار می‌کنه:
Bunny b3 = new Bunny("Bo") 
{
LikesCarrots = true,
LikesHumans = false
};


2️⃣ پشت صحنه چه خبره؟ (نکته حرفه‌ای) ⚙️
شاید فکر کنید این سینتکس فقط یه خلاصه نویسیه، ولی کامپایلر پشت صحنه یه کار هوشمندانه برای امنیت در برابر خطاها (Exception Safety) انجام میده.

اون اول آبجکت رو تو یه متغیر موقت میسازه و بعد پراپرتی‌ها رو ست می‌کنه. این کار تضمین می‌کنه که اگه وسط مقداردهی یکی از پراپرتی‌ها خطایی رخ بده، شما هیچوقت با یه آبجکت نصفه و نیمه و ناقص مواجه نمیشید!

🔖 هشتگ‌ها:
#CSharp #Programming #Developer #DotNet #OOP #CleanCode #BestPractices
🔬 کالبدشکافی آبجکت‌ها با Deconstructors در #C


تو پست قبلی دیدیم چطور با Object Initializers یه آبجکت رو به صورت شیک "بسازیم". حالا بیاید ببینیم چطور می‌تونیم یه آبجکت رو به همون زیبایی "بشکافیم" و اجزاش رو استخراج کنیم!

سی‌شارپ یه قابلیت مدرن و قدرتمند به اسم Deconstructor داره که دقیقاً برعکس سازنده (Constructor) عمل می‌کنه.

1️⃣ حالا Deconstructor چیست؟
این Deconstructor یه متد خاص به اسم Deconstruct هست که شما تو کلاستون تعریف می‌کنید. این متد، فیلدها و پراپرتی‌های کلاس شما رو به مجموعه‌ای از متغیرهای خروجی (out parameters) تبدیل می‌کنه.
public class Rectangle
{
public readonly float Width, Height;

public Rectangle(float width, float height)
{
Width = width;
Height = height;
}

// این متد Deconstructor ماست
public void Deconstruct(out float width, out float height)
{
width = Width;
height = Height;
}
}


2️⃣ جادوی سینتکس: کالبدشکافی در یک خط!
حالا که متد Deconstruct رو داریم، #C به ما یه سینتکس فوق‌العاده شیک و خوانا برای استفاده ازش میده:
var rect = new Rectangle(3, 4);

// جادو اینجا اتفاق میفته!
// این خط، متد Deconstruct رو صدا میزنه
(float width, float height) = rect;

Console.WriteLine($"Width: {width}, Height: {height}"); // خروجی: Width: 3, Height: 4
این سینتکس، کد شما رو به شدت تمیز و بیانگر می‌کنه.


3️⃣ ترفندهای خلاصه‌نویسی
این قابلیت چند تا ترفند برای خلاصه‌تر شدن هم داره:

استفاده از var:
// کامپایلر خودش نوع‌ها رو تشخیص میده
var (width, height) = rect;

نادیده گرفتن با _ (Discard):
اگه فقط به یکی از مقادیر نیاز دارید، می‌تونید اون یکی رو با _ نادیده بگیرید:
// ما اینجا فقط به ارتفاع نیاز داریم
var (_, height) = rect;

تخصیص به متغیرهای موجود:
اگه متغیرها رو از قبل دارید، دیگه نیازی به تعریفشون نیست:
float w, h;
(w, h) = rect;

🤔 حرف حساب و تجربه شما
شما تا حالا از Deconstructors تو کدهاتون استفاده کردید؟ به نظرتون بهترین کاربرد این قابلیت کجاست؟

🔖 هشتگ‌ها:
#CSharp #Programming #Developer #DotNet #ModernCSharp #CleanCode #BestPractices
🏷 فیلترهای کوئری نام‌دار در EF 10 (چندین فیلتر کوئری برای هر انتیتی)


فیلترهای کوئری سراسری (global query filters) در Entity Framework Core از دیرباز راهی مناسب برای اعمال شرایط مشترک به تمام کوئری‌های یک انتیتی بوده‌اند. این فیلترها به ویژه در سناریوهایی مانند حذف منطقی (soft deletion) 🗑 و چند-مستأجری (multi-tenancy) 🏢 مفید هستند، جایی که شما می‌خواهید همان دستور WHERE به صورت خودکار به هر کوئری اضافه شود.

با این حال، نسخه‌های قبلی EF Core از یک محدودیت بزرگ 😩 رنج می‌بردند: هر نوع انتیتی فقط می‌توانست یک فیلتر تعریف شده داشته باشد. اگر نیاز به ترکیب چندین شرط داشتید، یا باید عبارات && صریح می‌نوشتید یا فیلترها را به صورت دستی در کوئری‌های خاص غیرفعال و دوباره اعمال می‌کردید.

با EF 10، این وضعیت تغییر می‌کند.

قابلیت جدید فیلترهای کوئری نام‌دار (named query filters) به شما امکان می‌دهد چندین فیلتر را به یک انتیتی متصل کرده و با نام به آن‌ها ارجاع دهید. سپس می‌توانید فیلترهای فردی را در صورت نیاز غیرفعال کنید، به جای اینکه همه فیلترها را یکجا خاموش کنید.

بیایید این قابلیت جدید، چرایی اهمیت آن و چند روش عملی برای استفاده از آن را بررسی کنیم.

🔎فیلترهای کوئری (Query Filters) چه هستند؟

اگر مدتی است که از EF Core استفاده می‌کنید، ممکن است از قبل با فیلترهای کوئری سراسری آشنا باشید. یک فیلتر کوئری، شرطی است که EF به طور خودکار به تمام کوئری‌ها برای یک نوع انتیتی خاص اعمال می‌کند. در پشت صحنه، EF هر زمان که آن انتیتی کوئری می‌شود، یک دستور WHERE اضافه می‌کند. کاربردهای معمول عبارتند از:

• حذف منطقی: فیلتر کردن ردیف‌هایی که IsDeleted در آن‌ها true است.

• چند-مستأجری: فیلتر کردن بر اساس TenantId تا هر مستأجر فقط داده‌های خود را ببیند.

برای مثال، یک فیلتر حذف منطقی ممکن است اینگونه پیکربندی شود:
modelBuilder.Entity<Order>()
.HasQueryFilter(order => !order.IsDeleted);

با وجود این فیلتر، هر کوئری روی Orders به طور خودکار رکوردهای حذف شده منطقی را حذف می‌کند. برای شامل کردن داده‌های حذف شده، می‌توانید IgnoreQueryFilters() را روی کوئری فراخوانی کنید. عیب این کار این است که تمام فیلترهای روی آن انتیتی غیرفعال می‌شوند.

استفاده از چندین فیلتر کوئری

تاکنون، EF فقط یک فیلتر کوئری برای هر انتیتی مجاز می‌دانست. برای ترکیب فیلترها باید یک عبارت واحد با && می‌نوشتید:
modelBuilder.Entity<Order>()
.HasQueryFilter(order => !order.IsDeleted && order.TenantId == tenantId);

💡این کار می‌کند اما غیرفعال کردن انتخابی یک شرط را غیرممکن می‌سازد. EF 10 یک جایگزین بهتر معرفی می‌کند: فیلترهای کوئری نام‌دار.

برای متصل کردن چندین فیلتر به یک انتیتی، HasQueryFilter را با یک نام برای هر فیلتر فراخوانی کنید:
modelBuilder.Entity<Order>()
.HasQueryFilter("SoftDeletionFilter", order => !order.IsDeleted)
.HasQueryFilter("TenantFilter", order => order.TenantId == tenantId);

اکنون می‌توانید فقط فیلتر حذف منطقی را خاموش کنید در حالی که فیلتر مستأجر را فعال نگه می‌دارید:
// تمام سفارشات (شامل حذف شده‌های منطقی) برای مستأجر فعلی را برمی‌گرداند
var allOrders = await context.Orders
.IgnoreQueryFilters(["SoftDeletionFilter"])
.ToListAsync();


💡 نکته: استفاده از ثابت‌ها برای نام فیلترها
فیلترهای نام‌دار از کلیدهای رشته‌ای استفاده
می‌کنند. هاردکد کردن این نام‌ها می‌تواند باعث ایجاد "رشته‌های جادویی" (magic strings) شکننده شود. برای جلوگیری از این مشکل، ثابت‌ها را برای نام فیلترهای خود تعریف کنید.
public static class OrderFilters
{
public const string SoftDelete = nameof(SoftDelete);
public const string Tenant = nameof(Tenant);
}

modelBuilder.Entity<Order>()
.HasQueryFilter(OrderFilters.SoftDelete, order => !order.IsDeleted)
.HasQueryFilter(OrderFilters.Tenant, order => order.TenantId == tenantId);


یک رویه بهتر دیگر، پیچیدن فراخوانی ignore در یک متد توسعه (extension method) است:
public static IQueryable<Order> IncludeSoftDeleted(this IQueryable<Order> query)
=> query.IgnoreQueryFilters([OrderFilters.SoftDelete]);
جمع‌بندی 📝
معرفی فیلترهای کوئری نام‌دار در EF 10 یکی از محدودیت‌های دیرینه را برطرف می‌کند. شما اکنون می‌توانید:

🔹 چندین فیلتر را به یک انتیتی متصل کرده و آن‌ها را به صورت جداگانه مدیریت کنید.

🔹 فیلترهای خاصی را در یک کوئری LINQ با استفاده از IgnoreQueryFilters(["FilterName"]) به صورت انتخابی غیرفعال کنید.

🔹 الگوهای رایجی مانند حذف منطقی و چند-مستأجری را ساده‌سازی کنید.

فیلترهای کوئری نام‌دار می‌توانند به ابزاری قدرتمند برای تمیز نگه داشتن کوئری‌ها و کپسوله‌سازی منطق دامین شما تبدیل شوند.

آن‌ها را در نسخه پیش‌نمایش امتحان کنید و به این فکر کنید که چگونه می‌توانند پایگاه کد شما را ساده‌تر کنند.

🔖 هشتگ‌ها:
#CSharp #Programming #Developer #DotNet #EntityFrameWork #EF
🏢 معماری چند-مستأجره (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