🧱 سطح 1️⃣ : اضافه کردن ConcurrentDictionary
اولین فکری که احتمالاً به ذهنت میرسد این است که نرخها را داخل یک ConcurrentDictionary ذخیره کنی 🧠.
این ساختار thread-safe است، پس در نگاه اول ابزار درستی به نظر میرسد ✅.
private static readonly ConcurrentDictionary<string, decimal> Cache = new();
// In the Handler:
if (Cache.TryGetValue(currencyCode, out var cachedRate))
{
return cachedRate;
}
var rate = await currencyClient.GetExchangeRateAsync(currencyCode);
Cache.TryAdd(currencyCode, rate.Value);
این کار قطعاً تحت بار بالا به بهبود performance کمک میکند 🚀.
چندین thread میتوانند همزمان از دیکشنری بخوانند یا در آن بنویسند بدون اینکه برنامه کرش کند 💪.
اما ConcurrentDictionary فقط از ساختار دیکشنری محافظت میکند، نه از منطق شما ⚠️.
اگر ۱۰۰ کاربر دقیقاً در یک لحظه نرخ "EUR" را درخواست کنند 👥👥👥، TryGetValue برای همهی آنها false برمیگرداند.
در نتیجه، همهشان همزمان API را صدا میزنند 📡📡📡.
این یک race condition کلاسیک است 🏁.
شما حافظه را امن کردهاید، اما از API خارجی محافظت نکردهاید ❌.
یک مشکل دیگر هم وجود دارد:
نرخها هیچوقت expire نمیشوند ⏳❌.
⏰ سطح 2️⃣: اضافه کردن انقضای Cache
نرخ ارزها برای همیشه ثابت نمیمانند 💱.
ما به راهی نیاز داریم که بعد از مدتی آنها را منقضی کنیم.
از آنجایی که ConcurrentDictionary مفهوم Time To Live (TTL) ندارد،
باید دادههایمان را wrap کنیم 📦.
// Store both the rate and the time it was created
private record CacheEntry(decimal Rate, DateTime CreatedAt);
// Our cache now stores CacheEntry objects
private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
// Check: Is it there? And is it still "fresh"?
if (Cache.TryGetValue(currencyCode, out var entry) &&
(DateTime.UtcNow - entry.CreatedAt) < CacheDuration)
{
return entry.Rate;
}
حالا expiration داریم ✅.
اما در عوض، یک مشکل جدید ساختهایم 😬:
🐘 Thundering Herd (یا Cache Stampede)
هر ۵ دقیقه یکبار، وقتی cache منقضی میشود ⏱️،
تمام درخواستهای ورودی بهصورت همزمان دادهی «منقضیشده» میبینند
و همگی تلاش میکنند آن را refresh کنند 💥.
پس باید این مشکل را در مرحلهی بعدی حل کنیم.
🚦 سطح 3️⃣: حل مشکل «Cache Stampede»
برای حل این مشکل، باید مطمئن شویم که
فقط یک نفر اجازه دارد داده را بهروزرسانی کند
و بقیه منتظر بمانند ⏸️.
در #C چطور این کار را انجام میدهیم؟ 🤔
ما از SemaphoreSlim و الگویی به نام Double-Checked Locking استفاده میکنیم 🔐.
اول یک بار cache را چک میکنیم (مسیر سریع 🏃♂️)،
بعد lock میگیریم،
و سپس دوباره چک میکنیم تا ببینیم آیا در این فاصله thread دیگری cache را پر کرده یا نه.
// Basically a mutex but async-friendly
private static readonly SemaphoreSlim Lock = new(1, 1);
public static async Task<decimal> GetRateAsync(string code, CurrencyApiClient client)
{
// Fast path: No locking needed
if (Cache.TryGetValue(code, out var entry) && IsFresh(entry))
{
return entry.Rate;
}
var acquired = await Lock.WaitAsync(TimeSpan.FromSeconds(10)); // Avoid deadlocks
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Double-check: Did someone else finish the API call while we waited?
if (Cache.TryGetValue(code, out entry) && IsFresh(entry))
{
return entry.Rate;
}
var rate = await client.GetExchangeRateAsync(code);
var newEntry = new CacheEntry(rate.Value, DateTime.UtcNow);
// Atomically update the cache
// This is safe because we're inside the lock
Cache.AddOrUpdate(code, newEntry, (_, _) => newEntry);
return rate.Value;
}
finally
{
// Always release the lock
Lock.Release();
}
}
این کار یک بهبود واقعی است 👍.
اما هنوز یک حس بد وجود دارد… 😐
میتوانی مشکل این کد را پیدا کنی؟ 👀
🔒 این lock مثل یک global lock رفتار میکند.
یعنی اگر یک thread در حال گرفتن نرخ "EUR" باشد،
تمام threadهای دیگر (حتی آنهایی که "JPY" میخواهند 🇯🇵)
تا پایان درخواست "EUR" بلاک میشوند ⛔️.
این مشکل به lock contention معروف است ⚠️.
در مرحلهی بعدی، این مشکل را هم حل میکنیم 🛠✨.
🚀 سطح 4️⃣: مقیاسپذیری با Keyed Locking
حرکت «حرفهای» 👌 در اینجا استفاده از Keyed Locking است 🔑.
ما برای هر ارز مشخص یک lock جداگانه ایجاد میکنیم. از آنجایی که تعداد ارزها محدود است 💱، این روش از نظر مصرف حافظه سنگین نیست.
ما به یک ConcurrentDictionary اضافه نیاز داریم تا semaphoreها را بهازای هر کد ارز نگهداری کنیم 🧵.
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Locks = new();
// In the Handler:
var semaphore = Locks.GetOrAdd(currencyCode, _ => new SemaphoreSlim(1, 1));
if (!Cache.TryGetValue(currencyCode, out var cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
var acquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(10));
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Fetch and update logic...
}
finally { semaphore.Release(); }
}
تنها چیزی که تغییر کرده، نحوهی گرفتن lock است 🔒.
حالا اگر یک thread در حال گرفتن نرخ "EUR" باشد 🇪🇺، threadهای دیگر که "JPY" را درخواست کردهاند 🇯🇵، بدون معطلی ادامه میدهند ⏩.
این مقیاسپذیرترین نسخه از cache ماست 📈.
اما… ⚠️
این راهحل فقط در حافظه کار میکند.
پس برای سیستمهای توزیعشده یا چندین instance سرور مناسب نیست 🌐.
همچنین چند edge case دیگر هم وجود دارد که میتوانی آنها را بهعنوان تمرین بررسی کنی 🧠.
🧩 کد نهایی
در اینجا نسخهی نهایی منطق cache را میبینی:
public static class CurrencyConversion
{
private record CacheEntry(decimal Rate, DateTime CreatedAt);
private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Locks = new();
public static async Task<IResult> Handle(
string currencyCode,
decimal amount,
CurrencyApiClient currencyClient)
{
// Validate currency code format (3 uppercase letters)
if (string.IsNullOrWhiteSpace(currencyCode)||
currencyCode.Length != 3 ||
!currencyCode.All(char.IsLetter))
{
return Results.BadRequest(
new { error = "Currency code must be a 3-letter uppercase code (e.g., EUR, GBP)" });
}
// Validate amount (must be positive)
if (amount < 0)
{
return Results.BadRequest(new { error = "Amount must be a positive number" });
}
decimal? rate;
var semaphore = Locks.GetOrAdd(currencyCode, _ => new SemaphoreSlim(1, 1));
if (!Cache.TryGetValue(currencyCode, out var cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
var acquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(10));
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Double-check locking pattern: check again inside the lock
if (!Cache.TryGetValue(currencyCode, out cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
rate = await currencyClient.GetExchangeRateAsync(currencyCode);
if (rate == null)
{
return Results.NotFound(
new { error = $"Exchange rate for {currencyCode} not found or API error occurred" });
}
Cache.AddOrUpdate(currencyCode,
_ => new CacheEntry(rate.Value, DateTime.UtcNow),
(_, _) => new CacheEntry(rate.Value, DateTime.UtcNow));
}
else
{
rate = cachedRate!.Rate;
}
}
finally
{
semaphore.Release();
}
}
else
{
rate = cachedRate!.Rate;
}
var convertedAmount = amount * rate.Value;
return Results.Ok(new ExchangeRateResponse(
Currency: currencyCode,
BaseCurrency: "USD",
Rate: rate.Value,
Amount: amount,
ConvertedAmount: convertedAmount
));
}
}
گام بعدی این است که منطق اصلی cache را به یک کلاس reusable جداگانه استخراج کنی 🧩.
به این شکل میتوانی آن را در بخشهای دیگر برنامه هم استفاده کنی 🔁.
🎯 جمعبندی (Takeaway)
چرا اصلاً این همه دردسر؟ 🤔
دیدن یک ConcurrentDictionary ساده خیلی وسوسهبرانگیز است 😌 و فکر میکنی کار تمام شده.
اما همانطور که دیدیم، فاصلهی بین «کار میکند» و «مقیاسپذیر است»
پر از edge caseهایی است که میتوانند یک سیستم production را زمینگیر کنند 💥.
وقتی از یک library استفاده میکنی، این edge caseها برایت مدیریت میشوند 🛡.
اما ساختن آن با دست خودت، به تو سه ستون اصلی کد با کارایی بالا را یاد میدهد 🏛:
🧵 Thread safety
🔒 Lock contention
🛡 Resource protection
گاهی اوقات، «کسلکنندهترین» بخشهای زیرساخت، مثل cache،
در واقع جالبترین بخشها از نظر معماری هستند 🧠✨.
و آن ۱٪ مواقعی هم هست که به یک راهحل کاملاً سفارشی نیاز داری
که هیچ libraryای ارائهاش نمیدهد 🛠.
برای همین دانستن اصول پایه واقعاً ارزشمند است.
کتابخانههای مدرنی مثل HybridCache یا FusionCache این کارها را برایت انجام میدهند 🚀،
اما درک این الگوها باعث میشود دقیقاً بدانی چرا برنامهات تحت load آنطور که میبینی رفتار میکند 📊.
🧩 5️⃣ CQRS (Command Query Responsibility Segregation)(بخش پنجم)
الگوی CQRS یک الگوی طراحی است که عملیات خواندن (Query) را از عملیات نوشتن (Command) جدا میکند 🔀.
در این الگو، برای هرکدام مدل جداگانهای استفاده میشود:
📝 مدل نوشتن (Write Model): مسئول منطق کسبوکار و تغییر دادهها
📊 مدل خواندن (Read Model): بهینهشده برای خواندن، جستجو و گزارشگیری
⚙️ نحوهی کار (How it works)
✏️ ءCommandها نمایانگر عملیاتی هستند که وضعیت سیستم را تغییر میدهند و از طریق مدل نوشتن پردازش میشوند
🧠 مدل نوشتن قوانین کسبوکار را اعمال میکند، دادهها را اعتبارسنجی میکند و تغییرات را در پایگاهداده ذخیره میکند
🔍 ءQueryها داده را از طریق مدل خواندن بازیابی میکنند، بدون اینکه تغییری در وضعیت سیستم ایجاد شود
🗄 مدل خواندن معمولاً یک دیتابیس یا ساختار دادهی جداگانه است که برای عملکرد بهتر در Query بهینه شده
🔄 تغییراتی که در مدل نوشتن ایجاد میشوند، معمولاً بهصورت asynchronous و اغلب از طریق eventها به مدل خواندن منتقل میشوند
✅ مزایا (Benefits)
🚀 امکان بهینهسازی مستقل مدلهای خواندن و نوشتن بر اساس نیازهای عملکردی هرکدام
📈 قابلیت مقیاسپذیری جداگانهی عملیات خواندن و نوشتن، متناسب با الگوی مصرف واقعی
🧩 سادهتر شدن دامنههای پیچیده با جدا کردن منطق اعتبارسنجی Command از منطق Query
⚠️ معایب (Drawbacks)
⏳ ایجاد eventual consistency بین مدل نوشتن و مدل خواندن
🏗 افزایش پیچیدگی سیستم به دلیل وجود چندین مدل و مکانیزم همگامسازی
🔧 نیاز به زیرساخت اضافه برای همگام نگه داشتن مدلهای خواندن و نوشتن
🎯 موارد استفاده (Use cases)
🖥 اپلیکیشنهایی با بار خواندن و نوشتن بسیار متفاوت که نیاز به مقیاسپذیری مستقل دارند
🧠 دامنههای پیچیده که منطق کسبوکار در عملیات نوشتن تفاوت زیادی با نیازهای گزارشگیری دارد
📊 سیستمهایی با نسبت Read به Write بالا که عملکرد Query در آنها حیاتی است
تابآوری (Resiliency): طراحی سیستمهایی که خم میشوند اما نمیشکنند!
تو کتاب فارسی نمیدونم چندم دبستان بود که داستان یه درخت بلوط تنومند بود و چند شاخه نی، داستان اینطوری پیش میرفت که درخت به ریشهها و تنه تنومندش مینازید و برای زندگی نحیف نیها دلسوزی میکرد و میگفت شماها با هر باد خم میشید و هر چقدر اون باد ضعیف هم باشه شما رو خم میکنه، اما چیزی که درخت خبر نداشت انعطافپذیری نیها در برابر بادهای خیلی تند بود که میتونستند از پسش بربیان. که یک روز یکی از اون بادها درخت رو انداخت اما همچنان نیها برقرار بودند. 🌳
دنیای واقعی ذاتاً پر از آشوب است. شبکهها ناپایدارند، سرویسها از کار میافتند، وابستگیها خطا میدهند و این اتفاقها معمولاً در بدترین زمان ممکن رخ میدهند (یا ساعت پایانی کار روز چهارشنبه یا نصفه شب).
حتی مقاومترین سیستمهای دنیا نیز از شکست مصون نیستند. به همین دلیل، تمرکز صرف بر جلوگیری از خرابی کافی نیست؛ آنچه سیستمهای مدرن را متمایز میکند، تابآوری (Resiliency) آنهاست.
در حالی که Robustness (استحکام) تلاش میکند احتمال بروز خطا را کاهش دهد، Resiliency (تابآوری) بر این اصل بنا شده است که:
«خرابی اجتنابناپذیر است؛ مهم این است که سیستم چقدر سریع و هوشمند به حالت پایدار بازمیگردد.»
🪢 تابآوری چیست؟
تابآوری در معماری نرمافزار یعنی توانایی سیستم برای ادامهٔ ارائهٔ سرویس، حتی در شرایط شکست، و بازیابی سریع با حداقل اختلال برای کاربر.
🗯 یک سیستم تابآور:
- شکست را تشخیص میدهد
- آن را مهار میکند
- اجازه نمیدهد خرابی یک جزء، کل سیستم را از کار بیندازد
و در نهایت، خود را بازیابی میکند
سیستم تابآور مانند نیزار در برابر باد است: خم میشود، اما نمیشکند؛ و پس از طوفان دوباره سرپا میایستد.
💥 دو شاخص کلیدی برای اندازه گیری تابآوری:
1️⃣ RTO – Recovery Time Objective
حداکثر زمانی که یک سرویس باید در آن بازیابی شود تا اختلال ایجادشده غیرقابلقبول تلقی نشود.
ءRTO بسته به اهمیت سرویس متفاوت است و مرز بین «اختلال قابل قبول» و «Incident» را مشخص میکند.
2️⃣ MTTR – Mean Time To Repair/Recover
میانگین زمانی که طول میکشد تا یک سرویس پس از شکست به حالت عملیاتی بازگردد.
🌪 چرا تابآوری حیاتی است؟
تابآوری مستقیماً بر تجربهٔ کاربر و درآمد کسبوکار اثر میگذارد.
در بازارهای رقابتی، کاربر تحمل:
• لودینگهای طولانی
• درخواستهای معلق
• صفحات خطای مداوم
را ندارد؛ بهخصوص در روزهای اوج ترافیک مثل جمعه سیاه یا حراجیهای بهمن ماه.
از سوی دیگر، تابآوری مزایای مهمی برای تیمهای فنی دارد:
- کاهش Incidentهای ناگهانی
- فشار کمتر on-call
- تستپذیری و پایداری بالاتر
تابآوری اگر از ابتدا در طراحی لحاظ شود، بسیار کمهزینهتر و مؤثرتر از وصلهکاری بعد از بحران است.
الگوهای کلیدی تابآوری
🔹️ Circuit Breaker: قطع ارتباط موقت یک سرویس
🔸️ Fallback: نسخه از کش یا حذف از UI
🔹️ Bulkhead: جداسازی منابع مثلا گزارشگیریها
🔸️ Redundancy: استفاده از نسخههای جایگزین مخصوصا در معماری Loosely Coupled
🔹️ Fault Tolerance در کد
🔸️ Message Queue
🔹️ Rate Limiter
📌جمعبندی نهایی
تابآوری یعنی:
شکست یک جزء ≠ شکست کل سیستم
عملکرد اصلی باید همیشه زنده بماند
قابلیتهای جانبی باید قابل حذف، جایگزینی یا تضعیف باشند
هدف نهایی: ارائهٔ حداقل تجربهٔ قابل قبول در بدترین شرایط
یا به زبان سادهتر:
هیچوقت به کاربر خطای ۵۰۰ نشان نده؛
حتی وسط طوفان، بگذار سیستم خم شود، نه بشکند.
🔗Link
Microservices Design Patterns in .NET by Trevoir Williams
این کتاب جنبههای کلیدی معماری را پوشش میدهد:
🔹️طراحی مبتنی بر دامنه (DDD). نحوهی تعریف صحیح مرزهای سرویس و استفاده از Aggregates و Value Objects برای جلوگیری از ایجاد یک "distributed monolith".
🔹️ءCommunication Patterns. نگاهی دقیق به تعامل همزمان (HTTP/gRPC) و غیرهمزمان (Message Buses، RabbitMQ).
🔹️تابآوری و زیرساخت. پیادهسازی الگوهای Circuit Breaker، API Gateway و BFF، و همچنین رویکردهای مدرن استقرار با استفاده از Docker و Kubernetes.
چیزی که به ویژه برای تمرین و مصاحبه مفید است این است که بر موضوعاتی تمرکز میکند که اغلب در مورد آنها سوال میشود. به عنوان مثال، الگوی Saga به تفصیل توضیح داده شده است و تفاوتهای بین Choreography و Orchestration و همچنین نحوه پیادهسازی تراکنشهای توزیعشده را نشان میدهد. این کتاب همچنین gRPC را برای ارتباط مؤثر سرویس به سرویس پوشش میدهد.
این کتاب به وضوح نشان میدهد که چگونه نگرانیهای متقاطع (ثبت وقایع، نظارت، پروکسی) را به یک فرآیند جداگانه منتقل کنید تا منطق کسبوکار سرویس را بیش از حد بارگذاری نکنید.
⭐️ ءExtension Members ویژگی موردعلاقهی من در C# 14
ءExtension members ویژگی موردعلاقهی من در C# 14 هستند ✨
آنها یک تکامل مدرن از extension methodها محسوب میشوند که از نسخهی C# 3.0 در زبان وجود داشتند.
این سینتکس جدید باعث میشود بتوانید نهتنها متد، بلکه property و حتی static memberها را هم به typeهای موجود اضافه کنید 🧩
در این پست، موارد زیر را بررسی میکنیم 👇
• تفاوت کلیدواژهی extension با extension methodهای سنتی
• ساخت instance extension property
• اضافه کردن static extension member به typeها
• کار با genericها و type constraintها
• مثالهای واقعی از کاربرد extension memberها
• بهترین روشها برای سازماندهی کدهای extension
🚀 بزن بریم!
🔍 تفاوت Extension Keyword با Extension Methodهای سنتی
برنامهنویسهای #C از نسخهی C# 3.0 از extension methodها استفاده میکردند تا بدون تغییر سورسکد، به typeها قابلیت جدید اضافه کنند.
در روش سنتی، شما یک static class با static methodها میساختید.
قبل از C# 14، extensionها به این شکل نوشته میشدند 👇
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string value)
{
return string.IsNullOrEmpty(value);
}
public static string Truncate(this string value, int maxLength)
{
if (string.IsNullOrEmpty(value) value.Length <= maxLength)
{
return value;
}
return value.Substring(0, maxLength);
}
}
اما C# 14 کلیدواژهی جدید extension را معرفی میکند و یک رویکرد مدرنتر ارائه میدهد 🆕
در سینتکس جدید، receiver (typeای که میخواهید extend کنید) از memberهایی که اضافه میکنید جدا میشود.
بهجای اینکه روی هر متد this بنویسید، یک extension block تعریف میکنید که receiver فقط یکبار مشخص میشود 👇
public static class StringExtensions
{
extension(string value)
{
public bool IsNullOrEmpty()
{
return string.IsNullOrEmpty(value);
}
public string Truncate(int maxLength)
{
if (string.IsNullOrEmpty(value) value.Length <= maxLength)
{
return value;
}
return value.Substring(0, maxLength);
}
}
}
🔹 در اینجا:
• ءextension، receiver را بهعنوان پارامتر میگیرد
• داخل بلاک، متدها و propertyها دقیقاً مثل memberهای واقعی type نوشته میشوند
• پارامتر value در تمام memberهای داخل بلاک در دسترس است
📌 نکتهی مهم:
سینتکس قدیمی و جدید هر دو به کد یکسانی کامپایل میشوند، بنابراین رفتارشان دقیقاً یکسان است.
سینتکس جدید اختیاری است و در کنار روش قدیمی کار میکند.
🧱 ءExtension Memberهای پشتیبانیشده
ءExtension جدید از موارد زیر پشتیبانی میکند ✅
• Methods
• properties
• Static methods
• Static properties
🧩 ساخت Instance Extension Property
ءExtension propertyها باعث میشوند کد شما خواناتر و expressiveتر شود ✨
بهجای صدا زدن متد، میتوانید از propertyهایی استفاده کنید که طبیعیتر به نظر میرسند.
مثلاً هنگام کار با collectionها، خیلی وقتها چک میکنیم که خالی هستند یا نه.
بهجای اینکه همهجا بنویسیم ()items.Any!، میتوانیم یک property مثل IsEmpty بسازیم 👇
public static class CollectionExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
public bool HasItems => source.Any();
public int Count => source.Count();
}
}
public void ProcessOrders(IEnumerable<Order> orders)
{
if (orders.IsEmpty)
{
Console.WriteLine("No orders to process");
return;
}
foreach (var order in orders)
{
// Process order
}
}
➕ افزودن Static Extension Member به Typeها
ءStatic extensionها به شما اجازه میدهند بهجای instance، مستقیماً به خود type متد یا property اضافه کنید.
این نوع extension برای factory methodها 🏭 یا utility functionها 🧰 بسیار کاربردی است.
برای ساخت static extension، از extension بدون نامگذاری receiver استفاده میکنیم 👇
public static class ProductExtensions
{
extension(Product)
{
public static Product CreateDefault() =>
new Product
{
Name = "Unnamed Product",
Price = 0,
StockQuantity = 0,
Category = "Uncategorized",
CreatedDate = DateTime.UtcNow
};
public static bool IsValidPrice(decimal price) =>
price >= 0 && price <= 1000000;
public static string DefaultCategory => "General";
}
}
حالا میتوانی این memberهای static را مستقیماً روی خود type صدا بزنی 👇
var product = Product.CreateDefault();
if (Product.IsValidPrice(999.99m))
{
product.Price = 999.99m;
}
🧬 کار با Generics و Type Constraintها
ءGeneric extensionها به شما اجازه میدهند کدی بنویسید که با چندین type کار کند.
این موضوع بهخصوص هنگام کار با collectionها یا interfaceها بسیار مفید است 📦
بیایید <IEnumerable<T را extend کنیم و قابلیت فیلتر و تبدیل اضافه کنیم 👇
public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> source)
{
public IEnumerable<T> WhereNotNull() =>
source.Where(item => item != null);
public Dictionary<TKey, List<T>> GroupToDictionary<TKey>(
Func<T, TKey> keySelector) where TKey : notnull =>
source.GroupBy(keySelector)
.ToDictionary(g => g.Key, g => g.ToList());
}
}
اینجا از constraint زیر استفاده شده 👇
where TKey : notnull
📌 این تضمین میکند که کلید دیکشنری مقدار null نداشته باشد.
حالا میتوانی این extensionها را بهراحتی با LINQ chain کنی 🔗
var products = _productService.GetAll();
var productsByCategory = products
.WhereNotNull()
.Where(p => p.IsAvailable)
.GroupToDictionary(p => p.Category);
foreach (var category in productsByCategory)
{
Console.WriteLine($"{category.Key}: {category.Value.Count} products");
}
🔢 ءExtension با Constraint عددی
میتوانی constraint تعریف کنی تا extension فقط روی typeهای خاصی کار کند.
مثلاً این extension فقط روی typeهای عددی که <INumber<T را پیادهسازی کردهاند کار میکند 🧮
public static class NumericExtensions
{
extension<T>(IEnumerable<T> source)
where T : INumber<T>
{
public T Sum()
{
var total = T.Zero;
foreach (var item in source)
{
total += item;
}
return total;
}
public T Average()
{
var enumerable = source.ToList();
var sum = enumerable.Sum();
var count = T.CreateChecked(enumerable.Count);
return sum / count;
}
public IEnumerable<T> GreaterThan(T threshold) =>
source.Where(x => x > threshold);
}
}
این extension با هر type عددی سازگار است 👇
var prices = new[] { 10.99m, 25.50m, 5.00m, 15.75m };
var expensiveItems = prices.GreaterThan(15.00m);
var averagePrice = prices.Average();🌍 مثالهای واقعی از Extension Memberها
در Web APIها، معمولاً نیاز داریم اطلاعاتی از HttpContext استخراج کنیم.
بهجای تکرار کدهای مشابه، میتوانیم extension بنویسیم تا کد تمیزتر شود ✨
public static class ApiHttpContextExtensions
{
extension(HttpContext context)
{
public string CorrelationId =>
context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
public string ClientIp =>
context.Request.Headers["X-Forwarded-For"].FirstOrDefault()
?? context.Connection.RemoteIpAddress?.ToString()
?? "Unknown";
public bool IsApiRequest =>
context.Request.Path.StartsWithSegments("/api");
public string? GetBearerToken()
{
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader?.StartsWith("Bearer ") == true)
{
return authHeader.Substring("Bearer ".Length).Trim();
}
return null;
}
public T? GetQueryParameter<T>(string key)
{
if (context.Request.Query.TryGetValue(key, out var value))
{
try
{
return (T?)Convert.ChangeType(value.ToString(), typeof(T));
}
catch
{
return default;
}
}
return default;
}
public void AddResponseHeader(string key, string value)
{
context.Response.Headers[key] = value;
}
}
extension(HttpContext)
{
public static bool IsValidPath(string path) =>
!string.IsNullOrWhiteSpace(path) && path.StartsWith("/");
}
}
حالا middleware و controllerها بسیار خواناتر میشوند 🧼
public class RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation(
"Request {Method} {Path} from {ClientIp} with CorrelationId {CorrelationId}",
context.Request.Method,
context.Request.Path,
context.ClientIp,
context.CorrelationId);
context.AddResponseHeader("X-Correlation-ID", context.CorrelationId);
await _next(context);
}
}
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet]
public IActionResult GetOrders(HttpContext httpContext)
{
var pageSize = httpContext.GetQueryParameter<int?>("pageSize") ?? 10;
var pageNumber = httpContext.GetQueryParameter<int?>("page") ?? 1;
var orders = orderService.GetPaged(pageNumber, pageSize);
return Ok(orders);
}
}
📌 نتیجه؟
کد خواناتر 📖، تمیزتر 🧹 و بدون تکرار منطق پیچیدهی استخراج header و query parameterها، با defaultهای معقول ✅
✅ بهترین روشها برای سازماندهی کدهای Extension در C# 14 🧩✨
🗂 چطور Extension Blockها را سازماندهی کنیم؟
وقتی از کلیدواژه extension استفاده میکنید، میتوانید چندین extension member که روی یک type اعمال میشوند را داخل یک extension block قرار دهید.
این کار باعث کاهش تکرار 🔁 و کنار هم ماندن کدهای مرتبط میشود 🧠
همچنین میتوانید در یک static class چند extension block داشته باشید؛
مثلاً برای receiverهای متفاوت یا generic type parameterهای مختلف 👇
public static class CollectionExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
public bool HasItems => source.Any();
public int Count => source.Count();
}
extension(IEnumerable<string> source)
{
public string JoinWithComma() => string.Join(", ", source);
public IEnumerable<string> NonEmpty() => source.Where(s => !string.IsNullOrEmpty(s));
}
extension<T>(List<T> list)
{
public void AddIfNotExists(T item)
{
if (!list.Contains(item))
{
list.Add(item);
}
}
}
}
🔄 ترکیب syntax قدیمی و جدید
میتوانید syntax قدیمی (this) و syntax جدید (extension) را در یک کلاس با هم استفاده کنید 👍
public static class StringExtensions
{
// Traditional syntax
public static bool IsEmail(this string value)
{
return value.Contains("@");
}
// New syntax
extension(string value)
{
public bool IsUrl =>
Uri.TryCreate(value, UriKind.Absolute, out _);
public string ToTitleCase() =>
CultureInfo.CurrentCulture.TextInfo.ToTitleCase(value.ToLower());
}
}
🚫 از ساختن Extension Classهای غولپیکر اجتناب کنید
یک کلاس بزرگ با extension برای دهها type مختلف، بهمرور تبدیل به کابوس نگهداری میشود 😵💫
بهتر است extensionها را بر اساس domain یا کاربرد گروهبندی کنید 👇
public static class ProductExtensions
{
extension(Product product)
{
// Product-specific extensions
}
}
public static class ValidationExtensions
{
extension(Order value)
{
// Validation logic
}
extension(OrderItem value)
{
// Validation logic
}
}
📌 انتخاب ساختار مناسب کاملاً به نیاز پروژه بستگی دارد:
یا یک static class برای هر type
یا گروهبندی بر اساس functionality
🧮 برای مقادیر محاسباتی از Extension Property استفاده کنید
اگر extensionای دارید که پارامتری نمیگیرد و فقط یک مقدار برمیگرداند،
بهتر است بهجای method، آن را property تعریف کنید 👌
extension(Order order)
{
public decimal TotalPrice =>
order.Items.Sum(item => item.Price * item.Quantity);
}
✍️ نامگذاری معنادار انتخاب کنید
ءExtension propertyها باید طوری خوانده شوند که انگار عضو اصلی type هستند.
از نامهایی که «داد میزنند extension هستند» پرهیز کنید ❌
// Good
public bool IsEmpty => !source.Any();
public string DisplayPrice => price.ToString("C");
📚 ءExtensionها را مستند کنید
در پروژههای بزرگ، حتماً از XML Comment استفاده کنید تا بقیه اعضای تیم دقیقاً بدانند این extension چه کاری انجام میدهد 🧑💻📖
extension(Product product)
{
/// <summary>
/// Gets whether the product is currently available for purchase.
/// </summary>
/// <remarks>
/// A product is available if its stock quantity is greater than zero.
/// </remarks>
public bool IsAvailable => product.StockQuantity > 0;
}
🧾 جمعبندی
کلیدواژه extension در C# 14 اختیاری است ✅
تمام extension methodهای قبلی شما بدون هیچ تغییری همچنان کار میکنند.
اما زمانی که:
میخواهید property اضافه کنید 🧩
ءmemberهای static داشته باشید 🏗
یا کدهای مرتبط را تمیزتر و یکجا نگه دارید 🧹
ءsyntax جدید تجربه توسعهدهنده بسیار بهتری فراهم میکند ✨
اگر با C# 14 و NET 10. کار میکنید، حتماً extension propertyها را در پروژههایتان امتحان کنید.
بهزودی متوجه میشوید چقدر جا دارد methodها به property تبدیل شوند و کدها طبیعیتر و قابلفهمتر شوند 🧠💡
امیدوارم این مطلب براتون مفید بوده باشه 🙌
تا مطلب بعدی، موفق باشید 👋
پند روز جمعه: 🔥 یه نظر متفاوت درباره «دوباره اختراع کردن چرخ»:
شاید… واقعاً اتلاف وقتِ خوبی باشه ⏳😉
ادعای رایج منطقیه؛ مخصوصاً وقتی هدفت، تحویل سریع ارزش به کاربره 🚀
اما بیاید از یه زاویه دیگه نگاه کنیم 👀
برای یادگیری، میتونه فوقالعاده باشه.
بهخصوص وقتی اول مسیرت هستی 🌱
وقتی چیزی رو از صفر میسازی، حتی اگه هزار بار قبلاً ساخته شده باشه، مجبور میشی مفاهیم زیرساختی رو عمیقاً بفهمی 🧠
✨ چه اتفاقی میافته؟
• با تصمیمهای طراحی واقعی درگیر میشی 🧩
• با چالشهایی روبهرو میشی که اصلاً انتظارش رو نداشتی ⚠️
• و مهمتر از همه، واقعاً میفهمی چیزها «چطور» کار میکنن 🔍
🍳 بهش اینطوری فکر کن:
هیچکس فقط با خوندن کتاب آشپزی، سرآشپز حرفهای نمیشه.
باید دستهات رو توی آشپزخونه کثیف کنی! 👨🍳🔥
همین موضوع دقیقاً درباره مهندسی نرمافزار هم صدق میکنه 💻
اگه هدفت یادگیری عمیقتر و درک واقعی مفاهیمه، چرا خودت نسازی؟
🔄 الگوی (Saga Pattern) Saga (بخش ششم)
الگوی Saga Pattern برای مدیریت distributed transactions در چندین سرویس استفاده میشود. این الگو با شکستن یک تراکنش توزیعشده به دنبالهای از local transactions کار میکند.
هر local transaction فقط دادههای همان سرویس را تغییر میدهد و سپس یک event یا message منتشر میکند تا مرحله بعدی شروع شود 📣
اگر در هر مرحله خطایی رخ دهد، compensating transactions اجرا میشوند تا تغییرات مراحل قبلی را خنثی کنند و data consistency بین سرویسها حفظ شود 🔁
⚙️ ءSaga چگونه کار میکند؟
1️⃣ یک Saga زمانی شروع میشود که یک فرآیند تجاری نیاز به تغییر داده در چند سرویس داشته باشد
2️⃣ ءSaga coordinator تراکنش توزیعشده را به چند local transaction (یکی برای هر سرویس) تقسیم میکند
3️⃣ هر سرویس local transaction خودش را اجرا میکند و یک event مبنی بر موفقیت یا شکست منتشر میکند
4️⃣ ءCoordinator به این eventها گوش میدهد و مرحله بعدی را فعال میکند 👂
5️⃣ اگر همه مراحل با موفقیت انجام شوند، Saga پایان مییابد و تراکنش کامل تلقی میشود ✅
6️⃣ اگر هر مرحلهای شکست بخورد، compensating transactions بهترتیب معکوس اجرا میشوند تا تغییرات قبلی undo شوند ♻️
7️⃣ دو رویکرد اصلی برای پیادهسازی وجود دارد:
ءChoreography: سرویسها از طریق eventها با هم هماهنگ میشوند 🎭
ءOrchestration: یک coordinator مرکزی جریان کار را مدیریت میکند 🎼
🌟 مزایا (Benefits)
✔️ امکان اجرای distributed transactions در معماری microservices بدون نیاز به distributed locks یا two-phase commit
✔️ حفظ سازگاری دادهها با استفاده از compensating transactions بهجای rollbackهای سنگین
✔️ مناسب برای long-running business processes با تحمل خطای بالاتر 🛡
✔️ مقیاسپذیری بهتر، چون هر سرویس فقط مسئول local transaction خودش است 📈
⚠️ معایب (Drawbacks)
❌ پیچیدگی بالا در طراحی و پیادهسازی compensating transaction برای هر مرحله
❌ ایجاد eventual consistency که ممکن است باعث وضعیتهای موقتاً ناسازگار برای کاربر شود ⏳
❌ سختتر شدن debugging و monitoring چون تراکنشها در چند سرویس و در طول زمان پخش شدهاند
❌ سناریوهای پیچیدهی مدیریت خطا، مخصوصاً زمانی که خود compensating transaction هم شکست بخورد
🎯 موارد استفاده (Use cases)
📌 ءworkflowهای چندمرحلهای در سیستمهای enterprise که هر مرحله توسط یک سرویس جداگانه انجام میشود
📌 هر فرآیند تجاری که نیاز به تغییر هماهنگ دادهها در چند microservice دارد، بدون استفاده از distributed lock
✨ جمعبندی:
ءSaga Pattern یک راهحل کلیدی برای مدیریت تراکنشهای پیچیده در معماریهای توزیعشده است. اگرچه پیادهسازی آن ساده نیست، اما برای سیستمهای microservice-scale تقریباً اجتنابناپذیر است.
ما ۰.۱ ثانیه توی اجرا صرفهجویی کردیم، ولی ۳ روز از عمر تیم رو به باد دادیم!
چند وقت پیش توی پروژهای بودم که یکی از بچهها وسواس عجیبی روی سرعت داشت. ما یه کد تر و تمیز داشتیم که کار میکرد. ولی این همکارم گفتش که: "نه، این تابعی که نوشتی کنده! بذار پرفورمنسش رو ببرم بالا."
شروع کرد به تغییرات عجیب به بهانهی اینکه فراخوانی توابع سربار (Overhead) داره، توابعِ کوچیک و خوانا رو حذف کرد و همه رو ریخت توی یه تابعِ. شرطهای if/else شفاف رو تبدیل کرد به یک خط محاسبهی ریاضی پیچیده تا CPU کمتری مصرف بشه.
ما اولش هیجانزده بودیم و میگفتیم که دمت گرم! الان چون کد کوتاهتر شده و تابع کمتر صدا زده میشه، حتما درخواستا خیلی سریع پردازش میشن.
نتیجه؟ کاربر نهایی اصلا متوجه اون سرعت ناچیز نشد (چون گلوگاه اصلا اونجا نبود). اما چند وقت بعد، وقتی بیزینس یه تغییر کوچیک خواست، ما فلج شدیم.
کدی که قبلا توی ۱۰ دقیقه ادیت میشد، حالا شده بود یه میدون مین. هیچکس نمیفهمید اون خط طولانی ریاضی دقیقا داره چیکار میکنه. ما خوانایی رو قربانی یه سرعت توهمی کرده بودیم.
اینجا بود که یاد جملهی معروف دونالد کنوث افتادم: "بهینهسازی زودهنگام (Premature Optimization)، ریشهی تمام دردسرهاست."
ما داشتیم دقیقا همین مصیبت رو سر خودمون میاوردیم و قانونِ طلایی Uncle Bob رو فراموش کرده بودیم:
قانون ۱۰ به ۱: ما ۱۰ برابر زمانی که کد مینویسیم، صرف خوندن کد میکنیم. اون کدی که دوستمون زد، شاید نوشتنش ۱ ساعت طول کشید، ولی خوندن و دیباگ کردنش ۱۰ ساعت از وقت کل تیم رو گرفت. هر خط کدی که به بهانهی Performance ناخوانا میشه، داره هزینهی نگهداری رو تصاعدی میبره بالا.
ما یاد گرفتیم بهینهسازی واقعی این نیست که کد رو فشرده کنی. بهینهسازی واقعی اینه که کدی بنویسی که نفر بعدی (یا خود ۶ ماه بعدت) بتونه راحت بخونه و توسعهش بده.
چند وقت پیش توی پروژهای بودم که یکی از بچهها وسواس عجیبی روی سرعت داشت. ما یه کد تر و تمیز داشتیم که کار میکرد. ولی این همکارم گفتش که: "نه، این تابعی که نوشتی کنده! بذار پرفورمنسش رو ببرم بالا."
شروع کرد به تغییرات عجیب به بهانهی اینکه فراخوانی توابع سربار (Overhead) داره، توابعِ کوچیک و خوانا رو حذف کرد و همه رو ریخت توی یه تابعِ. شرطهای if/else شفاف رو تبدیل کرد به یک خط محاسبهی ریاضی پیچیده تا CPU کمتری مصرف بشه.
ما اولش هیجانزده بودیم و میگفتیم که دمت گرم! الان چون کد کوتاهتر شده و تابع کمتر صدا زده میشه، حتما درخواستا خیلی سریع پردازش میشن.
نتیجه؟ کاربر نهایی اصلا متوجه اون سرعت ناچیز نشد (چون گلوگاه اصلا اونجا نبود). اما چند وقت بعد، وقتی بیزینس یه تغییر کوچیک خواست، ما فلج شدیم.
کدی که قبلا توی ۱۰ دقیقه ادیت میشد، حالا شده بود یه میدون مین. هیچکس نمیفهمید اون خط طولانی ریاضی دقیقا داره چیکار میکنه. ما خوانایی رو قربانی یه سرعت توهمی کرده بودیم.
اینجا بود که یاد جملهی معروف دونالد کنوث افتادم: "بهینهسازی زودهنگام (Premature Optimization)، ریشهی تمام دردسرهاست."
ما داشتیم دقیقا همین مصیبت رو سر خودمون میاوردیم و قانونِ طلایی Uncle Bob رو فراموش کرده بودیم:
قانون ۱۰ به ۱: ما ۱۰ برابر زمانی که کد مینویسیم، صرف خوندن کد میکنیم. اون کدی که دوستمون زد، شاید نوشتنش ۱ ساعت طول کشید، ولی خوندن و دیباگ کردنش ۱۰ ساعت از وقت کل تیم رو گرفت. هر خط کدی که به بهانهی Performance ناخوانا میشه، داره هزینهی نگهداری رو تصاعدی میبره بالا.
ما یاد گرفتیم بهینهسازی واقعی این نیست که کد رو فشرده کنی. بهینهسازی واقعی اینه که کدی بنویسی که نفر بعدی (یا خود ۶ ماه بعدت) بتونه راحت بخونه و توسعهش بده.
یه زمانی فکر میکردم هرچی async/await تو کدم بیشتر باشه، سیستم مثل مووووشک کار میکنه
یعنی کد رو باز میکردی، همهجا async، همهجا await…
با خودم میگفتم: «این دیگه تهشه، این سیستم موشکه.»
بعد سیستم رفت زیر بار…
موشک بوووود ولی همون اول ترکید.
همهچی ظاهراً درست بود.
دیدم CPU داره کارشو میکنه، رم هم داشت نفس میکشید، لاگها هم چیز خاصی نمیگفتن.
ولی response time داشت زیاد میشد، کاربرها غر میزدن، منم داشتم به مانیتور زل میزدم که «مگه async نبود؟»
اونجا بود که فهمیدم async قرار نیست بیاد پیشونیتو ماچ کنه و بگه تو نگران نباش همه چی با من.
درواقع async فقط میگه: «داداش من منتظرم، اون Thread بیصاحبو ول کن بره یه کار دیگه بکنه.»
ولی مشکل از جایی شروع میشه که وسط این asyncهای خوشگیلی موووشگیل،
یه کار CPU-bound بندازی.
یا بدتر…
یه .Result یا .Wait بزنی و بگی: «ستون وایسا کارت دارم.»
اینجاست که Thread Pool میاد بیرون میگه یه لیوان آب به من بده ناموسن
میبینی Threadها یکییکی مفقود الاثر میشن
تمام Requestهای جدید میرسن، میبینن جا نیست، چهار زانو میشینن دم در سرور.
سیستم از بیرون مثل بنز جی کلاس، از تو شاهین اتومات.
بدترین قسمتش چیه؟
هیچ اروری نمیبینی.
نه exception، نه crash، نه هیچی.
فقط کاربره که هر ۵ ثانیه یه بار رفرش میزنه و مرده و زندتو مورد دعای خیر قرار میده.
اونجا بود که فهمیدم async بودن به معنی سریع بودن نیست.
اگر منتظر دیتابیس، شبکه یا دیسکی، async عالیه.
اگر داری CPU رو به خاک میدی، async فقط یه اسم قشنگه روش.
و اگر این دوتا رو قاطی کنی، Thread Pool که دیگه واویلا.
از اون روز به بعد، قبل از اینکه async بنویسم، از خودم میپرسم:
الان واقعاً منتظرم؟
یا فقط دارم خودمو گول میزنم؟
خیلی وقتا مشکل performance این نیست که کدت بده.
مشکل اینه که فکر میکنی async یعنی «همه چی حل شد».
نه.
درواقع async ابزار حرفهایه،
ولی اگه نفهمی چی دستته، خیلی سریع تبدیل میشه به سلاح کشتار جمعی.
یعنی کد رو باز میکردی، همهجا async، همهجا await…
با خودم میگفتم: «این دیگه تهشه، این سیستم موشکه.»
بعد سیستم رفت زیر بار…
موشک بوووود ولی همون اول ترکید.
همهچی ظاهراً درست بود.
دیدم CPU داره کارشو میکنه، رم هم داشت نفس میکشید، لاگها هم چیز خاصی نمیگفتن.
ولی response time داشت زیاد میشد، کاربرها غر میزدن، منم داشتم به مانیتور زل میزدم که «مگه async نبود؟»
اونجا بود که فهمیدم async قرار نیست بیاد پیشونیتو ماچ کنه و بگه تو نگران نباش همه چی با من.
درواقع async فقط میگه: «داداش من منتظرم، اون Thread بیصاحبو ول کن بره یه کار دیگه بکنه.»
ولی مشکل از جایی شروع میشه که وسط این asyncهای خوشگیلی موووشگیل،
یه کار CPU-bound بندازی.
یا بدتر…
یه .Result یا .Wait بزنی و بگی: «ستون وایسا کارت دارم.»
اینجاست که Thread Pool میاد بیرون میگه یه لیوان آب به من بده ناموسن
میبینی Threadها یکییکی مفقود الاثر میشن
تمام Requestهای جدید میرسن، میبینن جا نیست، چهار زانو میشینن دم در سرور.
سیستم از بیرون مثل بنز جی کلاس، از تو شاهین اتومات.
بدترین قسمتش چیه؟
هیچ اروری نمیبینی.
نه exception، نه crash، نه هیچی.
فقط کاربره که هر ۵ ثانیه یه بار رفرش میزنه و مرده و زندتو مورد دعای خیر قرار میده.
اونجا بود که فهمیدم async بودن به معنی سریع بودن نیست.
اگر منتظر دیتابیس، شبکه یا دیسکی، async عالیه.
اگر داری CPU رو به خاک میدی، async فقط یه اسم قشنگه روش.
و اگر این دوتا رو قاطی کنی، Thread Pool که دیگه واویلا.
از اون روز به بعد، قبل از اینکه async بنویسم، از خودم میپرسم:
الان واقعاً منتظرم؟
یا فقط دارم خودمو گول میزنم؟
خیلی وقتا مشکل performance این نیست که کدت بده.
مشکل اینه که فکر میکنی async یعنی «همه چی حل شد».
نه.
درواقع async ابزار حرفهایه،
ولی اگه نفهمی چی دستته، خیلی سریع تبدیل میشه به سلاح کشتار جمعی.
🧩 7️⃣ الگوی Sidecar (Sidecar Pattern) (بخش هفتم)
ءSidecar Pattern یک کامپوننت کمکی را در کنار container اصلی اپلیکیشن شما deploy میکند که وظیفه ارائه قابلیتهای پشتیبان مانند logging، monitoring، configuration یا networking را بر عهده دارد. 🛠
ءSidecar در همان محیط اجرایی اپلیکیشن اصلی اجرا میشود و lifecycle مشترکی با آن دارد. این موضوع باعث میشود بتوان بدون تغییر کد اپلیکیشن، قابلیتهای جدیدی به آن اضافه کرد. 🔌
⚙️ نحوه کار (How it works):
یک sidecar container در کنار container اصلی اپلیکیشن و در همان container یا host اجرا میشود 🧱
هر دو container از یک network namespace مشترک استفاده میکنند و میتوانند از طریق localhost با هم ارتباط برقرار کنند 🌐
ءSidecar میتواند ترافیک ورودی و خروجی را intercept کند و مسئول cross-cutting concernهایی مانند logging، جمعآوری metrics یا قابلیتهای service mesh باشد 📊
ارتباط بین اپلیکیشن و sidecar از طریق local network calls انجام میشود 🔄
قابلیتهای sidecar میتوانند مستقل از اپلیکیشن بهروزرسانی شوند، بدون نیاز به تغییر یا redeploy کد اصلی ♻️
امکان اتصال چند sidecar به یک اپلیکیشن واحد برای اهداف مختلف وجود دارد 🧩🧩
✅ مزایا (Benefits):
جداسازی دغدغههای زیرساختی از کد اپلیکیشن و تمرکز کد بر business logic 🧠
پشتیبانی از polyglot architecture، چون sidecar مستقل از زبان یا framework اپلیکیشن عمل میکند 🌍
سادهسازی توسعه اپلیکیشن با استفاده از کامپوننتهای زیرساختی قابل استفاده مجدد ♻️
امکان بهروزرسانی مستقل قابلیتهای زیرساختی بدون redeploy اپلیکیشن 🚀
کاهش code duplication بین سرویسها با متمرکز کردن قابلیتهای مشترک در sidecar 📦
⚠️ معایب (Drawbacks):
افزایش مصرف منابع، چون هر instance اپلیکیشن یک یا چند sidecar اضافی اجرا میکند 💾
افزایش پیچیدگی در پیکربندی deployment و orchestration 🧩
احتمال افزایش latency به دلیل hopهای شبکهای اضافی بین اپلیکیشن و sidecar ⏱️
🎯 موارد استفاده (Use cases):
• پیادهسازی Service Mesh برای مدیریت ترافیک، امنیت و observability 🔐📈
• ءCentralized logging که در آن sidecar لاگهای اپلیکیشن را جمعآوری و به سیستمهای logging ارسال میکند 📝
• ءConfiguration management که sidecar پیکربندیها را بهصورت داینامیک دریافت و بهروزرسانی میکند ⚙️
• یکپارچهسازی Event-driven که در آن sidecar مدیریت messaging را از طریق queueهای قابل تعویض (مثل Kafka، RabbitMQ، Azure Service Bus) انجام میدهد و امکان تغییر آنها بدون دست زدن به کد اپلیکیشن را فراهم میکند (برای مثال با استفاده از Dapr) 🔄