یه زمانی فکر میکردم هرچی 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) 🔄
چالش برنامهنویسی🔥
یک شیء در یک static field نگهداری میشود و در هیچ جای دیگری استفاده نمیشود.
رفتار Garbage Collector نسبت به این شیء چیست؟ An object is referenced only by a static field and nowhere else. What will the Garbage Collector do?
یک شیء در یک static field نگهداری میشود و در هیچ جای دیگری استفاده نمیشود.
رفتار Garbage Collector نسبت به این شیء چیست؟ An object is referenced only by a static field and nowhere else. What will the Garbage Collector do?
Anonymous Quiz
13%
Collect it immediately
17%
Collect it in Gen 0
62%
Never collect it
7%
Throw a runtime exception
یک نکتهی ظریف در EF Core که میتونه برنامهتون رو کرش بده ⚠️
اگه با EF Core کار کرده باشید، احتمالاً خیلی وقتها برای کلید اصلی این مدلو انتخاب کردید:
یعنی مقدار Id توسط دیتابیس (مثلاً SQL Server با IDENTITY) ساخته میشه.
تا اینجا همهچیز کاملاً استاندارده.
اما اینجا یه دام خیلی ظریف وجود داره 👇
فرض کنید در Domain Layer اومدید برای Id یک ولیدیشن گذاشتید:
یا حتی در setter:
در ظاهر کاملاً منطقیه:
«شناسه نباید منفی یا صفر باشه» ✅
ولی برنامهتون موقع ()SaveChanges کرش میکنه! 💥
ءEF Core وقتی یک entity جدید رو Add میکنه، هنوز هیچ Id واقعیای نداره.
اما برای اینکه بتونه Change Tracking و Relationship ها رو مدیریت کنه، مجبور میشه:
"یک Temporary Key براش بسازه."
و این کلید موقت معمولاً:
• عدد منفیه
• فقط داخل حافظه است
• هرگز وارد دیتابیس نمیشه
مثلاً:
• ءFKها رو ست کنه
• ءGraph از entityها رو track کنه
• بعد از Insert، مقدار واقعی رو جایگزین کنه
چون شما در Domain گفتید:
«ءId منفی غیرقانونیه»
ولی EF میگه:
«من مجبورم موقتاً منفی بذارم»
و این دو تا با هم invariant همدیگه رو میشکنن.
EF مقدار منفی ست میکنه →
Domain validation تریگر میشه →
Exception →
Crash 😬
این یه مثال عالیه از این اصل مهم در DDD:
ءDomain نباید به مکانیزمهای زیرساختی وابسته باشه
و مهمتر:
ءId دیتابیس نباید identity دامنه باشد
1️⃣ اصلاً روی Id دیتابیس ولیدیشن نذار 🚫
بهترین و رایجترین راه:
چون:
ءDomain نباید به persistence concern حساس باشه
ءId دیتابیس یک detail فنیه، نه business rule
2️⃣ از Surrogate Key در دامنه استفاده نکن 🔑
به جای این:
از identityهایی استفاده کن که در Domain تولید میشوند
(Guid/Ulid/ValueObject)
یعنی:
ءId دامنه = چیزی که خودت میسازی
ءId دیتابیس = فقط برای storage
3️⃣ اگر خیلی وسواسی هستی: Validation رو بعد از Persist انجام بده
مثلاً:
ولی این عملاً یعنی پذیرفتی که:
ءId قبل از persistence معتبر نیست
که باز هم یعنی validation روش منطقی نیست.
این مشکل یه نمونهی کلاسیک از تضاد بین این دو دنیاست:
Domain World ORM World
Invariants Temporary keys
Business meaning Technical identity
Stability Internal mutations
و دقیقاً نشون میده چرا:
نباید مفاهیم دیتابیس رو مستقیم وارد دامنه کنیم
اگر از ValueGeneratedOnAdd یا IDENTITY استفاده میکنید:
❌ هرگز روی Id در دامنه validation نگذارید
❌ هرگز Id دیتابیس رو business concept در نظر نگیرید
در عوض:
✅ دامنه = با مفاهیم بیزینسی (Value Objects)
✅ دیتابیس = فقط ابزار ذخیرهسازی
و این جمله رو همیشه یادت باشه:
ءTemporary keys در EF یک persistence concern هستند،
و نباید وارد invariants دامنه شوند.
این دقیقاً همون جاییه که مهندسی نرمافزار از «کدنویسی» جدا میشه 🚀
اگه با EF Core کار کرده باشید، احتمالاً خیلی وقتها برای کلید اصلی این مدلو انتخاب کردید:
builder.Property(x => x.Id)
.ValueGeneratedOnAdd();
یعنی مقدار Id توسط دیتابیس (مثلاً SQL Server با IDENTITY) ساخته میشه.
تا اینجا همهچیز کاملاً استاندارده.
اما اینجا یه دام خیلی ظریف وجود داره 👇
سناریوی مشکلدار 🧨
فرض کنید در Domain Layer اومدید برای Id یک ولیدیشن گذاشتید:
public class Order
{
public int Id { get; private set; }
public Order()
{
if (Id <= 0)
throw new DomainException("Id must be positive");
}
}
یا حتی در setter:
set
{
if (value <= 0)
throw new DomainException();
_id = value;
}
در ظاهر کاملاً منطقیه:
«شناسه نباید منفی یا صفر باشه» ✅
ولی برنامهتون موقع ()SaveChanges کرش میکنه! 💥
دلیل واقعی چیـه؟ (رفتار داخلی EF) 🧠
ءEF Core وقتی یک entity جدید رو Add میکنه، هنوز هیچ Id واقعیای نداره.
اما برای اینکه بتونه Change Tracking و Relationship ها رو مدیریت کنه، مجبور میشه:
"یک Temporary Key براش بسازه."
و این کلید موقت معمولاً:
• عدد منفیه
• فقط داخل حافظه است
• هرگز وارد دیتابیس نمیشه
مثلاً:
Order.Id = -2147482647
✨️ءEF از این مقدار استفاده میکنه تا:
• ءFKها رو ست کنه
• ءGraph از entityها رو track کنه
• بعد از Insert، مقدار واقعی رو جایگزین کنه
پس چرا برنامه کرش میکنه؟ 💣
چون شما در Domain گفتید:
«ءId منفی غیرقانونیه»
ولی EF میگه:
«من مجبورم موقتاً منفی بذارم»
و این دو تا با هم invariant همدیگه رو میشکنن.
💡نتیجه:
EF مقدار منفی ست میکنه →
Domain validation تریگر میشه →
Exception →
Crash 😬
این دقیقاً چه درسی به ما میده؟ 🎯
این یه مثال عالیه از این اصل مهم در DDD:
ءDomain نباید به مکانیزمهای زیرساختی وابسته باشه
و مهمتر:
ءId دیتابیس نباید identity دامنه باشد
راهحلهای درست (Professional Approaches) 🧩
1️⃣ اصلاً روی Id دیتابیس ولیدیشن نذار 🚫
بهترین و رایجترین راه:
// No validation on Id at all
public int Id { get; private set; }
چون:
ءDomain نباید به persistence concern حساس باشه
ءId دیتابیس یک detail فنیه، نه business rule
2️⃣ از Surrogate Key در دامنه استفاده نکن 🔑
به جای این:
Order.Id (int identity)
از identityهایی استفاده کن که در Domain تولید میشوند
(Guid/Ulid/ValueObject)
یعنی:
ءId دامنه = چیزی که خودت میسازی
ءId دیتابیس = فقط برای storage
3️⃣ اگر خیلی وسواسی هستی: Validation رو بعد از Persist انجام بده
مثلاً:
public bool IsPersisted => Id > 0;
ولی این عملاً یعنی پذیرفتی که:
ءId قبل از persistence معتبر نیست
که باز هم یعنی validation روش منطقی نیست.
نکتهی خیلی عمیق معماری 🧠🔥
این مشکل یه نمونهی کلاسیک از تضاد بین این دو دنیاست:
Domain World ORM World
Invariants Temporary keys
Business meaning Technical identity
Stability Internal mutations
و دقیقاً نشون میده چرا:
نباید مفاهیم دیتابیس رو مستقیم وارد دامنه کنیم
جمعبندی طلایی 🏆
اگر از ValueGeneratedOnAdd یا IDENTITY استفاده میکنید:
❌ هرگز روی Id در دامنه validation نگذارید
❌ هرگز Id دیتابیس رو business concept در نظر نگیرید
در عوض:
✅ دامنه = با مفاهیم بیزینسی (Value Objects)
✅ دیتابیس = فقط ابزار ذخیرهسازی
و این جمله رو همیشه یادت باشه:
ءTemporary keys در EF یک persistence concern هستند،
و نباید وارد invariants دامنه شوند.
این دقیقاً همون جاییه که مهندسی نرمافزار از «کدنویسی» جدا میشه 🚀
🔖هشتگها:
#EFCore #DDD #CleanArchitecture #ORM #DomainModel #SoftwareEngineering #DotNet
حل مشکل Invalidating کش توزیعشده با Redis و HybridCache 🧠🚀
سیستمهای توزیعشده برای مقیاسپذیری عالی هستند، اما یک کلاس کاملاً جدید از مشکلات را هم معرفی میکنند. یکی از سختترین این مشکلات، cache invalidation است. ⚠️
در NET 9.، مایکروسافت کتابخانهای به نام HybridCache معرفی کرد تا فرآیند caching را سادهتر کند. این یک کتابخانه فوقالعاده است که سرعت کش در حافظه (L1) را با پایداری کش توزیعشده (L2) مثل Redis ترکیب میکند. همچنین بهصورت پیشفرض از محافظت در برابر cache stampede پشتیبانی میکند. 🛡
اما یک نکته وجود دارد. ❗️
وقتی چندین instance از اپلیکیشن خود را اجرا میکنید، HybridCache بهصورت خودکار کش محلی L1 را بین تمام نودها همگامسازی نمیکند. اگر دادهای روی Node A بهروزرسانی شود، Node B همچنان دادهی قدیمی را از کش درونحافظهای خودش برمیگرداند تا زمانی که آن entry منقضی شود. ⏳
در حالی که HybridCache یک گام بزرگ رو به جلو است، نبود یک backplane داخلی برای invalidation یک محدودیت شناختهشده است. در واقع، یک بحث فعال در ریپازیتوری GitHub مربوط به dotnet/extensions وجود دارد که دقیقاً همین feature request را دنبال میکند. تا زمانی که این قابلیت ارائه شود، مجبوریم خودمان راهحل بسازیم. 🛠
در این خبرنامه بررسی میکنیم:
• معضل کش توزیعشده 🌍
• چرا HybridCache بهتنهایی این مشکل را حل نمیکند 🤔
• استفاده از Redis Pub/Sub بهعنوان backplane 📡
• پیادهسازی invalidation بلادرنگ کش ⚡️
بیایید شروع کنیم. 👇
معضل کش توزیعشده (The Distributed Caching Dilemma)
بیایید یک سناریوی معمول در محیط production را تصور کنیم. شما یک API دارید که روی چندین سرور (یا pod) پشت یک load balancer اجرا میشود. ⚙️
برای بهبود performance، از caching استفاده میکنید. سرعت حافظه محلی را میخواهید، پس از HybridCache استفاده میکنید. 🚀
سناریوی شکست به این صورت است:
• کاربر A پروفایل خود را روی Server 1 بهروزرسانی میکند. 👤
• ءServer 1 دیتابیس را آپدیت میکند و کش محلی خودش را پاک میکند. 🗑
• کاربر A (یا کاربر B) به Server 2 درخواست میزند. 🔁
• ءServer 2 هنوز دادهی قدیمی پروفایل را در HybridCache محلی خودش نگه داشته است. 🧊
• کاربر اطلاعات قدیمی را میبیند، چون کش محلی invalidate نشده است. 😕
چرا فقط مدت زمان کش را کوتاه نکنیم؟ ⏱️🤔
یک راهحل «هکی» و رایج برای حل این مشکل این است که مدت زمان کش L1 (TTL) را خیلی کم کنیم. مثلاً تنظیم کنیم که کش محلی هر ۱۰ ثانیه منقضی شود.
در حالی که این کار بازهی ناسازگاری را کمتر میکند، اما واقعاً مشکل را حل نمیکند؛ فقط آن را پنهان میکند. 🎭
این رویکرد دو مشکل جدید ایجاد میکند:
1️⃣ افزایش Latency 🚦: حالا اپلیکیشن شما خیلی بیشتر مجبور است به کش توزیعشده L2 (مثل Redis) یا حتی دیتابیس درخواست بزند.
2️⃣ از دست رفتن Efficiency 📉: مزیت اصلی کش L1 این است که کلاً از درخواست شبکه جلوگیری میکند. اگر دادهها خیلی سریع منقضی شوند، برای بخش عمدهای از ترافیک، این مزیت performance را از دست میدهید.
برای چیزهایی مثل مجوزهای کاربر (user permissions)، feature flagها یا قیمتگذاری، «تقریباً درست» معمولاً کافی نیست. شما به سازگاری فوری (immediate consistency) نیاز دارید. ⚡️
راهحل: Redis Pub/Sub بهعنوان Backplane 📡🧩
برای حل این مشکل، به یک backplane نیاز داریم.Backplane یک کانال ارتباطی است که تمام نودهای اپلیکیشن ما را به هم وصل میکند.
وقتی یک cache entry روی یک نود حذف یا بهروزرسانی میشود، یک پیام روی backplane منتشر میکنیم. تمام نودهای دیگر مشترک (subscribe) این کانال هستند و به محض دریافت پیام، کلید مربوطه را از کش محلی خودشان حذف میکنند. 🗑
ءRedis از قبل انتخاب محبوبی برای کش L2 است، بنابراین کاملاً منطقی است که از قابلیت Pub/Sub آن برای این مکانیزم سیگنالدهی استفاده کنیم. 🔔
این فرآیند به این شکل کار میکند:
🔸️ءPublisher 🧑💻: نودی که داده را تغییر میدهد، یک پیام invalidation شامل cache key منتشر میکند.
🔹️ءSubscriber 👂: تمام نودها روی این کانال گوش میدهند.
🔸️ءAction ⚙️: وقتی پیام میرسد، متد HybridCache.RemoveAsync(key) را صدا میزنند.
پیادهسازی راهحل ⚙️
ما به کتابخانهی StackExchange.Redis برای مدیریت پیامرسانی نیاز داریم.
بیایید با تعریف یک سرویس ساده برای انتشار (publishing) شروع کنیم. این سرویس مسئول اطلاع دادن به بقیهی سیستم است که یک کلید تغییر کرده است. 🔔
public interface ICacheInvalidator
{
Task InvalidateAsync(string key, CancellationToken cancellationToken = default);
}
public class RedisCacheInvalidator(
IConnectionMultiplexer connectionMultiplexer,
ILogger<RedisCacheInvalidator> logger)
: ICacheInvalidator
{
private const RedisChannel Channel = RedisChannel.Literal("cache-invalidation");
public async Task InvalidateAsync(string key, CancellationToken cancellationToken = default)
{
var subscriber = connectionMultiplexer.GetSubscriber();
await subscriber.PublishAsync(Channel, new RedisValue(key));
logger.LogInformation("Published invalidation for key: {Key}", key);
}
}
حالا هر زمان که یک entity را در Command Handler یا Service آپدیت میکنید، کافی است متد ICacheInvalidator.InvalidateAsync را صدا بزنید. 🚀
public class UpdateUserProfileHandler(
AppDbContext dbContext,
ICacheInvalidator cacheInvalidator,
ILogger<UpdateUserProfileHandler> logger)
{
public async Task Handle(int userId, string newName, CancellationToken ct)
{
// 1. Update the database
var user = await dbContext.Users.FindAsync([userId], ct);
if (user is null)
{
return;
}
user.Name = newName;
await dbContext.SaveChangesAsync(ct);
// 2. Invalidate the cache (Distributed)
var cacheKey = $"user:{userId}";
await cacheInvalidator.InvalidateAsync(cacheKey, ct);
logger.LogInformation("Updated user and invalidated cache for {UserId}", userId);
}
}
Background Listener 🧵
در مرحلهی بعد، به یک background service نیاز داریم که روی هر نود اجرا شود. این سرویس روی کانال Redis مشترک میشود و کلیدها را از HybridCache محلی حذف میکند.
یک نکتهی سریع دربارهی self-publishing:
چون Redis Pub/Sub پیام را برای همهی subscriberها ارسال میکند، نودی که خودش پیام invalidation را منتشر کرده هم دوباره همان پیام را دریافت میکند. در این پیادهسازی، ما دوباره همان کلید را حذف میکنیم. این کار تکراری ولی بیضرر است و کد را ساده نگه میدارد. 🔁
توجه کنید که ما مستقیماً HybridCache را داخل background service تزریق کردهایم. یک جایگزین این است که از IMemoryCache استفاده کنیم، چون همان L1 cache داخل HybridCache است.
public class CacheInvalidationService(
IConnectionMultiplexer connectionMultiplexer,
HybridCache hybridCache,
ILogger<CacheInvalidationService> logger)
: BackgroundService
{
private const RedisChannel Channel = RedisChannel.Literal("cache-invalidation");
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var subscriber = connectionMultiplexer.GetSubscriber();
await subscriber.SubscribeAsync(Channel, (channel, value) =>
{
string key = value.ToString();
logger.LogInformation("Invalidating local cache for: {Key}", key);
// This removes the item from the local L1 cache
var task = hybridCache.RemoveAsync(key, stoppingToken);
if (!task.IsCompleted)
{
task.GetAwaiter().GetResult();
}
});
}
}
وصل کردن همهچیز به هم (Wiring It All Together) 🔌
در نهایت باید این سرویسها را داخل DI container ثبت کنیم:
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect("<REDIS_CONNECTION_STRING>"));
// Register HybridCache (defaults generally work fine for L1)
builder.Services.AddHybridCache();
// Register our invalidation services
builder.Services.AddSingleton<ICacheInvalidator, RedisCacheInvalidator>();
builder.Services.AddHostedService<CacheInvalidationService>();
حالا وقتی Node A متد
InvalidateAsync("user:123")
را صدا بزند، Redis این پیام را به Node B، Node C و بقیه میفرستد. همهی آنها متد
hybridCache.RemoveAsync("user:123")
را اجرا میکنند و تضمین میشود که درخواست بعدی دادهی تازه را از منبع اصلی (یا از L2 مشترک) بگیرد. 🔄✨
یک راه بهتر: FusionCache 🧠🔥
اگر ساختن backplane اختصاصی حس «reinventing the wheel» به شما میدهد، بهتر است نگاهی به FusionCache بیندازید.
ءFusionCache یک کتابخانهی بالغ و battle-tested است که سالهاست دقیقاً همین مشکل را حل کرده. این کتابخانه یک backplane داخلی دارد که به صورت خودکار Pub/Sub را مدیریت میکند.
حتی بهتر از آن: FusionCache اخیراً یک پیادهسازی از abstract class مربوط به HybridCache اضافه کرده است. یعنی میتوانید تقریباً بدون تغییر زیاد در کد فعلی، آن را جایگزین کنید. 😍
// Using FusionCache's implementation of HybridCache
builder.Services.AddFusionCache()
.WithBackplane(
new RedisBackplane(new RedisBackplaneOptions { Configuration = "<REDIS_CONNECTION_STRING>" }))
.AsHybridCache();
جمعبندی 📝
ءHybridCache یک اضافهشدن بسیار قدرتمند به اکوسیستم NET. است و عملاً مزایای IMemoryCache و IDistributedCache را با هم ترکیب میکند. اما در سناریوهای multi-node که نیاز به consistency بالا دارند، هنوز به یک مکانیزم برای همگامسازی کشهای محلی نیاز دارید.
ءRedis Pub/Sub یک راهحل سبک و مؤثر برای این مشکل ارائه میدهد. 📡
با پیادهسازی یک «bus» ساده برای پیامهای invalidation، به بهترین حالت ممکن میرسید:
هم performance فوقالعادهی کش محلی را دارید، هم consistency سیستم توزیعشده را. ⚡️🌍
موفق باشید😉🚀
🔖هشتگها:
#DotNet #HybridCache #Redis #DistributedSystems #Caching #SystemDesign #FusionCache
Forwarded from DotNet | دات نت
🔥 نکته حیاتی در Hangfire: جنگ با ساعت و تایمزونها! ⏰🌍
اگر تا حالا براتون پیش اومده که جابی رو برای ساعت ۸ صبح تنظیم کردید ولی ساعت ۱۱:۳۰ اجرا شده، این پست برای شماست!
⚠️ ماجرا چیه؟
هنگفایر (Hangfire) به صورت پیشفرض (Default) همه جابهای تکرارشونده (Recurring Jobs) رو بر مبنای ساعت UTC اجرا میکنه. یعنی اگر تنظیمات تایمزون رو بهش ندید، باید اختلاف ساعت ایران با گرینویچ رو دستی حساب کنید که اصلا جالب نیست.
حالا اگر بخوایم بگیم "به وقت ایران اجرا شو"، با یه چالش جدید روبرو میشیم:
🔸 ویندوز میگه: "Iran Standard Time"
🔸 لینوکس/داکر میگه: "Asia/Tehran"
اگر این تفاوت هندل نشه، روی سرور لینوکسی یا کانتینر داکر به خطای TimeZoneNotFoundException میخورید! 🤯
✅ راه حل نهایی (Cross-Platform):
با این تیکه کد، هم مشکل UTC رو حل کنید و هم کدی بنویسید که روی ویندوز، لینوکس و مک بدون تغییر کار کنه:
💡 با این روش، دیگه نگران جلو/عقب کشیدن ساعتها یا تفاوت محیط لوکال و سرور نباشید.
🔗 بحث مرتبط در کامیونیتی هنگفایر:
https://discuss.hangfire.io/t/need-local-time-instead-of-utc/279/7
🎺 برای یادگیری بیشتر و دریافت مطالب مفید در زمینه .NET و برنامهنویسی، به کانال ما بپیوندید!
📚💻 @dotnetcode🖥
#CSharp #DotNet #Hangfire #Docker #Backend #TimeZone #Tips
اگر تا حالا براتون پیش اومده که جابی رو برای ساعت ۸ صبح تنظیم کردید ولی ساعت ۱۱:۳۰ اجرا شده، این پست برای شماست!
⚠️ ماجرا چیه؟
هنگفایر (Hangfire) به صورت پیشفرض (Default) همه جابهای تکرارشونده (Recurring Jobs) رو بر مبنای ساعت UTC اجرا میکنه. یعنی اگر تنظیمات تایمزون رو بهش ندید، باید اختلاف ساعت ایران با گرینویچ رو دستی حساب کنید که اصلا جالب نیست.
حالا اگر بخوایم بگیم "به وقت ایران اجرا شو"، با یه چالش جدید روبرو میشیم:
🔸 ویندوز میگه: "Iran Standard Time"
🔸 لینوکس/داکر میگه: "Asia/Tehran"
اگر این تفاوت هندل نشه، روی سرور لینوکسی یا کانتینر داکر به خطای TimeZoneNotFoundException میخورید! 🤯
✅ راه حل نهایی (Cross-Platform):
با این تیکه کد، هم مشکل UTC رو حل کنید و هم کدی بنویسید که روی ویندوز، لینوکس و مک بدون تغییر کار کنه:
using System.Runtime.InteropServices;
// 1. تشخیص خودکار شناسه تایمزون بر اساس سیستمعامل
// Windows -> "Iran Standard Time"
// Linux/Docker -> "Asia/Tehran"
var tehranId = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "Iran Standard Time"
: "Asia/Tehran";
// 2. تنظیم ساعت به وقت ایران
RecurringJob.AddOrUpdate(
type.FullName,
() => job.ExecuteAsync(),
attribute.CronExpression,
new RecurringJobOptions
{
// خداحافظ UTC، سلام تهران! 👋
TimeZone = TimeZoneInfo.FindSystemTimeZoneById(tehranId)
}
);
💡 با این روش، دیگه نگران جلو/عقب کشیدن ساعتها یا تفاوت محیط لوکال و سرور نباشید.
🔗 بحث مرتبط در کامیونیتی هنگفایر:
https://discuss.hangfire.io/t/need-local-time-instead-of-utc/279/7
📚💻 @dotnetcode
#CSharp #DotNet #Hangfire #Docker #Backend #TimeZone #Tips
Please open Telegram to view this post
VIEW IN TELEGRAM
Hangfire Discussion
Need local time instead of UTC
I need to schedule recurring jobs based on local time, for example 02:00 CET (which is UTC+1 normally and UTC+2 during daylight savings time aka “summer time”). I know this introduces some problems when switching to/from summer to winter time (there is an…
چالش برنامه نویسی امروز🔥
خروجی چی میشه؟
لطفا پاسختون رو کامنت کنید😊
خروجی چی میشه؟
لطفا پاسختون رو کامنت کنید😊
Anonymous Quiz
14%
A B
16%
B A
16%
B B
54%
A A
📌برسی تفاوت Task و ValueTask
زمانیکه تصمیم میگیریم کدهای زده شده را بهینه کنیم، اکثرا دنبال راه حلهای جدید نمیگردیم. این مورد کاملا غریزی است؛ چرا که بهدنبال کمترین انرژی و بیشترین بازدهی هستیم؛ این طبیعت انسان است. صرفا کدهای قبلی را بازبینی میکنیم و سعی میکنیم نحوهی نوشتن منطقهای موجود را بهینه کنیم. در همین راستا درک عملکرد Task و ValueTask ها شاید قدمی مهم در مورد بهینه کردن کدها باشد؛ چرا استفاده درست و بجای این دو مورد میتواند تاثیر زیادی بر روی سرعت و استفاده از مصرف حافظه داشته باشد؟ در این مقاله سعی میکنیم تا درک درستی از این دو داشته باشیم.
✨️ء<Task<T چیست؟
ءTask یک کلاس در فضای نام System.Threading.Tasks است؛ بهطوریکه کمک میکند تا یک قسمت از برنامه به صورت مستقل از Thread اصلی اجرا شود. بهبیان دیگر میتواند یک Thread Pool را ایجاد و با توجه به روند کار، از یک مرحلهی اجرایی به مرحلهای دیگر منتقل میکند. همچنین هر Task میتواند یک مقدار برگشتی نیز داشته باشد.
این درحالیاست که میتواند صرفا یک فرآیند را اجرا کند، بدون اینکه خروجی داشته باشد. بهعبارتی دیگر اگر فرآیندی داشته باشیم که در نهایت یک شناسه را برمیگرداند، از <Task<int و اگر فرآیندی داشته باشیم که صرفا فرآیند همگام سازی دادههای قدیمی به جدید را انجام میدهد، میتواند از نوع Task باشد.
همانطور که اشاره شد، Task یک کلاس است که شامل متدها و فیلدهای مختلفی میباشد. با استفاده از این اعضا میتوان نحوهی اجرای کدها و وضعیتهای مختلف اجرای آن را مدیریت کرد، تا در نهایت اجرای آن کامل شود.
به دلیل اینکه Task یک class است و class ها از نوع ReferenceType میباشند، روی حافظهی Heap ذخیره میشوند و بهازای هر بار فراخوانی متدی که خروجی Task دارد، شیء Task را روی Heap ذخیره میکند. این شیء وضعیت اجرای قسمتی از کد ما را که میتواند sync یا async باشد، در خود ذخیره میکند تا در نهایت اجرای آن کامل شود.
💡نحوه استفاده از <Task<T
برای درک بهتر، یک تکه کد را با بهره بردن از Task ایجاد میکنیم :
public static class DummyWeatherProvider
{
public static async Task<Weather> Get(string city)
{
await Task.Delay(10);
var weather = new Weather
{
City = city,
Date = DateTime.Now,
AvgTempratureF = new Random().Next(5, 70)
};
return weather;
}
}
همان طور که مشخص است، کلاس موجود یک متد به نام Get دارد تا اطلاعات آب و هوای شهر مورد نظر را به صورت یک Task برگرداند. حال کد زیر را جهت بررسی تغییر وضعیتهای اجرایی این Task ایجاد میکنیم :
static async Task CheckTaskStatus()
{
var task = DummyWeatherProvider.Get("Stockholm");
LogTaskStatus(task.Status);
await task;
LogTaskStatus(task.Status);
}
static void LogTaskStatus(TaskStatus status)
{
Console.WriteLine($"Task Status: {Enum.GetName(typeof(TaskStatus), status)}");
}
ءTaskStatus یک enumeration است، بهطوریکه بیانگر وضعیتهای مختلف یک Task در حال اجرا میباشد. برای مثال: WaitingForActivation, Running, RanToCompletion. در کد بالا ابتدا متد را فراخوانی میکنیم. سپس منتظر میمانیم تا متد اجرا شده، تکمیل شود. در اولین لاگ وضعیت، به WaitingForActivation و در دومین لاگ به RanToCompletion تبدیل میشود. حالکه با Task ها و نحوهی اجرای فرآیند آن آشنا شدیم، در قسمت بعدی به بررسی ValueTask ها میپردازیم.
✨️ء<ValueTask<T چیست؟
همانند تسک ها ، ValueTask هم برای مدیریت وضعیت فرآیند استفاده میشود؛ با این تفاوت که ValueTask ها از نوع struct هستند. بهطوریکه نحوهی ذخیره سازی آنها در حافظه به نسبت class ها کاملا متفاوت است. از نقطه نظر سرعت، تشخیص دادن اینکه کدامیک باید استفاده شود، باید با توجه به سناریو، بررسی و انتخاب شود؛ چرا که از نظر تخصیص حافظه متفاوت عمل میکنند. برای درک بهتر عملکرد ValueTask ها کد زیر را بررسی میکنیم :
public class WeatherService
{
private readonly ConcurrentDictionary<string, Weather> _cache;
public WeatherService()
{
_cache = new();
}
public async Task<Weather> GetWeatherTask(string city)
{
if (!_cache.ContainsKey(city))
{
var weather = await DummyWeatherProvider.Get(city);
_cache.TryAdd(city, weather);
}
return _cache[city];
}
public async ValueTask<Weather> GetWeatherValueTask(string city)
{
if (!_cache.ContainsKey(city))
{
var weather = await DummyWeatherProvider.Get(city);
_cache.TryAdd(city, weather);
}
return _cache[city];
}
کلاس WeatherService شامل یک فیلد private از نوع collection و دو متد است. ما از _cache جهت نگهداری اطلاعاتی که قبلا دریافت شده، استفاده میکنیم و به نوعی in-memory cache را پیاده سازی میکنیم. پیاده سازی منطق هر دو متد GetWeatherTask و GetWeatherValueTask کاملا شبیه به هم است؛ بهطوریکه اول بررسی میکنیم اطلاعات آب و هوای شهر مورد نظر در _cache وجود دارد یا خیر؟ اگر وجود داشت، اطلاعات به صورت مستقیم برگشت داده میشود؛ در غیر این صورت ()DummyWeatherProvider.Get فراخوانی خواهد شد.
در قدم بعدی اطلاعات بهدست آمده را در cache_ ذخیره میکنیم. سپس مقدار ذخیره شده را برگشت میدهیم. در واقع تنها تفاوت دو متد ذکر شده، نوع خروجی آن میباشد؛ یکی از Taskو دیگری از ValueTask استفاده میکند.
برای مقایسهی مصرف حافظهی این دو روی هر دو متد، Benchmark میگیریم. برای پیاده سازی نیار به کدهای زیر داریم :
csharp
[MemoryDiagnoser]
public class TaskAndValueTaskBenchmark
{
private readonly WeatherService _weatherService;
public TaskAndValueTaskBenchmark()
{
_weatherService = new();
}
[Benchmark]
[Arguments("Denver")]
public async Task<Weather> TaskBenchmark(string city)
{
return await _weatherService.GetWeatherTask(city);
}
[Benchmark]
[Arguments("London")]
public async ValueTask<Weather> ValueTaskBenchmark(string city)
{
return await _weatherService.GetWeatherValueTask(city);
}
}
نتیجه به دست آمده به شرح زیر است :
TaskBenchmark
{
"Gen0" : "0.0229",
"Allocated" : "144 B"
}
ValueTaskBenchmark
{
"Gen0" : "----",
"Allocated" : "------"
}
با توجه به نتیجه بهدست آمده، متدی که خروجی ValueTask دارد، حافظهای را تخصیص ندادهاست؛ این دقیقا مزیت مهم ValueTask نسبت به Task میباشد.
🛠مزیت <ValueTask<T
بهدلیل اینکه از نوع struct هستند، بر روی حافظه، در قسمت Stack ذخیره میشوند و به صورت خودکار بعد از اینکه نیازی به آنها نباشد، از حافظه حذف میشوند . به همین دلیل به شکل قابل توجهی، فشار را از روی GC کاهش میدهد .
علاوه بر این، در سناریویی که اکثر کدها به صورت sync اجرا میشوند، در این مواقع استفاده از ValueTask، بهتر از Task میباشد .
این سری متد GetWeatherValueTask را جهت تشخص اینکه اغلب کدها به صورت sync یا async اجرا میشوند، بررسی میکنیم. در متد ذکر شده اگر اطلاعات شهر مورد نظر وجود داشته باشد، کار به صورت sync اجرا میشود و اگر شهر وجود نداشته باشد، کار به صورت async اجرا میشود. با بررسی دقیقتر متوجه میشویم اکثر مواقع در این متد کار به صورت sync اجرا میشود؛ چرا که بعد ازدریافت اطلاعات، مجدد آن را دریافت نمیکند، بلکه از حافظه میخواند (همان cache_ ) .
🔸️محدودیتهای استفاده از <ValueTask<T
1️⃣ در اینجا تنها یکبار امکان استفاده از await وجود دارد. وقتی یکبار valueTask را await میکنیم، بهتر است کار دیگری بر روی آن انجام ندهیم؛ چراکه ممکن است از حافظه پاک شده باشد.
2️⃣ اگر در سناریویی لازم دارید چندین بار await را بر روی valueTask اجرا کنید، لازم است ابتدا آن را به Task تبدیل کنیم. برای این کار متد AsTask را فراخوانی میکنیم (بهتر است صرفا یکبار متد AsTask را فراخوانی کنیم).
3️⃣ نمیتوانیم به یک ValueTask به صورت هم زمان در حالت MultiThreads دسترسی داشته باشیم.
4️⃣ به صورت پیش فرض خروجی عملیات async، نوع Task میباشد؛ مگر اینکه اغلب مراحل کار به صورت sync اجرا شود، مانند مثالی که بالاتر اشاره شد.
🔖هشتگها:
#Task #ValueTask
یک دموی عملی از مهاجرت بدون داونتایم با استفاده از Password Hashing 🔐
نیازهای امنیتی به مرور زمان تغییر میکنند. چیزی که پنج سال پیش «به اندازهی کافی امن» محسوب میشد، ممکن است امروز حتی از یک ممیزی امنیتی هم رد نشود. ⚠️
شما باید به یک الگوریتم مدرن مثل Argon2 یا Bcrypt مهاجرت کنید.
اما اینجا یک مشکل اساسی وجود دارد: هش کردن یک عملیات یکطرفه است. شما نمیتوانید هشهای قبلی را برعکس کنید تا آنها را «ارتقا» دهید. 🔁❌
اگر به سادگی پیادهسازی IPasswordHasher را عوض کنید، برنامه را میشکنید.
تمام کاربران فعلی که تلاش میکنند لاگین کنند با خطا مواجه میشوند، چون hasher جدید فرمت قدیمی را نمیفهمد. 🚫
در این مقاله میخواهم مفهوم zero-downtime migration را به صورت عملی نشان بدهم. 🛠
سیستمهای واقعی محدودیتهای بیشتری دارند (و شما نباید سیستم احراز هویت را از صفر خودتان بنویسید).
اما این یک مثال تمیز از یک الگو است که میتوانید برای مهاجرت دیتابیس دوباره استفاده کنید:
• مهاجرت از فرمت قدیمی به فرمت جدید
• حفظ رفتار قبلی سیستم
• مهاجرت تدریجی دادهها
• حذف legacy فقط وقتی کار تمام شد
بیایید شروع کنیم. 🚀
The Naive Approach and Why It Fails
رویکرد سادهلوحانه و اینکه چرا شکست میخورد ❌
فرض کنید یک سیستم احراز هویت ساده دارید.
میخواهید hasher قدیمی PBKDF2 را با یک پیادهسازی استاندارد Argon2 جایگزین کنید.
ممکن است فکر کنید:
«فقط پیادهسازی جدید را در dependency injection ثبت میکنم.»
// Switching from LegacyHasher to ModernHasher
builder.Services.AddSingleton<IPasswordHasher, ModernHasher>();
سناریوی شکست این است:
🔸️کاربران جدید:
ثبتنام میکنند و بدون مشکل لاگین میشوند. پسورد آنها از روز اول با Argon2 هش شده است. ✅
🔸️کاربران قدیمی:
کاربر پسورد درست را وارد میکند. سیستم هش قدیمی PBKDF2 را از دیتابیس میگیرد. 📦
🔸️فاجعه:
ءModernHasher تلاش میکند هش PBKDF2 را verify کند. فوراً fail میشود و 401 Unauthorized برمیگرداند. 💥
شما ناخواسته کل کاربران فعلی سیستم را لاک کردهاید.
ما به راهی نیاز داریم که هر دو الگوریتم همزمان پشتیبانی شوند، بدون اینکه کد لاگین به یک آشغال غیرقابل نگهداری تبدیل شود. 🧨
The Solution: Migration on Login
راهحل: مهاجرت در زمان لاگین 🔄
استراتژی ساده است:
ما دیتابیس را با یک batch job مهاجرت نمیدهیم.
ما کاربران را lazy و زمانی که هویت خودشان را ثابت میکنند مهاجرت میدهیم.
جریان کار به این شکل است:
Attempt 1:
تلاش کن پسورد را با الگوریتم جدید verify کنی. 🥇
Attempt 2 (Fallback):
اگر شکست خورد، بررسی کن ببین الگوریتم قدیمی میتواند آن را verify کند یا نه. 🥈
The Migration:
اگر verification با الگوریتم قدیمی موفق بود:
• کاربر را لاگین کن (موفقیت) ✅
• بلافاصله پسورد را با الگوریتم جدید دوباره هش کن 🔐
• رکورد دیتابیس را آپدیت کن 🗄
از این به بعد، لاگینهای بعدی این کاربر از مسیر استاندارد جدید عبور میکنند. 🎯
Implementation with .NET Keyed Servicesدر NET 8.، مایکروسافت قابلیتی به نام Keyed Services معرفی کرد که برای این سناریو کاملاً ایدهآل است. این قابلیت به ما اجازه میدهد چند پیادهسازی از یک interface یکسان را ثبت کنیم و بر اساس نام (کلید) آنها را دریافت کنیم. 🔑
پیادهسازی با استفاده از Keyed Services در NET. 🧩
1️⃣ Registering the Services
ما هر دو hasher را در فایل Program.cs ثبت میکنیم و به هرکدام یک کلید منحصربهفرد میدهیم:
// Register the implementations with specific keys
builder.Services.AddKeyedSingleton<IPasswordHasher, Pbdkf2PasswordHasher>("legacy");
builder.Services.AddKeyedSingleton<IPasswordHasher, Argon2PasswordHasher>("modern");
// (Optional) Register the modern one as the default for other services
builder.Services.AddSingleton<IPasswordHasher, Argon2PasswordHasher>();
2️⃣ The Login Command Handler
حالا منطق مهاجرت را پیادهسازی میکنیم. هر دو hasher را با استفاده از اتریبیوت [FromKeyedServices] تزریق میکنیم. 🧪
public class LoginCommandHandler(
IUserRepository userRepository,
[FromKeyedServices("modern")] IPasswordHasher newHasher,
[FromKeyedServices("legacy")] IPasswordHasher legacyHasher)
{
public async Task<AuthenticationResult> Handle(LoginCommand command)
{
var user = await userRepository.GetByEmailAsync(command.Email);
if (user is null)
{
return AuthenticationResult.Fail();
}
// 1. Try the new algorithm first (Happy Path)
if (newHasher.Verify(user.PasswordHash, command.Password))
{
return AuthenticationResult.Success(user);
}
// 2. Fallback: Check if it's a legacy hash
if (legacyHasher.Verify(user.PasswordHash, command.Password))
{
// 3. MIGRATION STEP: Re-hash and save
var newHash = newHasher.Hash(command.Password);
user.UpdatePasswordHash(newHash);
await userRepository.SaveChangesAsync();
return AuthenticationResult.Success(user);
}
return AuthenticationResult.Fail();
}
}
این کد تضمین میکند که کاربران فعال به صورت خودکار ارتقا پیدا کنند.
بعد از چند ماه، بخش عمدهای از کاربران شما روی الگوریتم جدید خواهند بود. 📈
Real-World Improvements
بهبودهای دنیای واقعی 🌍
در حالی که پیادهسازی بالا کار میکند، دو بهبود وجود دارد که آن را production-ready میکند.
1️⃣ Algorithm Prefixes
پیشوند الگوریتمها
اتکا به روش «trial and error» برای verify کردن کار میکند، اما تمیزتر این است که دقیقاً بدانیم هر هش با چه الگوریتمی ساخته شده است.
الگوریتمهای استاندارد معمولاً یک prefix دارند
(مثلاً Bcrypt با $2a$ یا $2b$ شروع میشود).
میتوان از این موضوع برای مسیریابی بهینه استفاده کرد:
public bool IsLegacyHash(string hash)
{
// This assumes we're storing a prefix for PBKDF2 hashes. Something to consider.
return hash.StartsWith("pbkdf2$");
}
مزیت دیگر این کار این است که میتوانیم مستقیماً از دیتابیس کوئری بگیریم و کاربرانی که هنوز روی فرمت قدیمی هستند را پیدا کنیم. 🔍
2️⃣ Feature Flags
فلگهای ویژگی 🚩
انجام یک write در دیتابیس هنگام لاگین باعث افزایش latency میشود. اگر ترافیک بالایی دارید، بهتر است کنترل این rollout را در دست بگیرید.
با قرار دادن منطق مهاجرت پشت یک Feature Flag، میتوانید در صورت بالا رفتن فشار روی دیتابیس، مرحلهی نوشتن را غیرفعال کنید، در حالی که کاربران همچنان از مسیر fallback میتوانند لاگین کنند.
if (await featureManager.IsEnabledAsync(FeatureFlags.MigratePasswords) &&
legacyHasher.Verify(user.PasswordHash, command.Password))
{
// Perform migration...
}
Finishing the Migration
پایان دادن به مهاجرت 🏁
بعد از مدتی (معمولاً چند ماه)، اکثر اکانتهای فعال ارتقا پیدا میکنند.
سپس میتوانید یک اسکریپت cleanup اجرا کنید تا هشهای قدیمی باقیمانده را شناسایی کنید و کاربران مربوطه را مجبور کنید در لاگین بعدی پسورد خود را ریست کنند.
در این نقطه میتوانید موارد زیر را حذف کنید:
• ثبت legacy hasher
• مسیر verification مربوط به legacy
• ءfeature flag
و مهاجرت کامل میشود. ✅
جمعبندی 🧠
یک ارتقای «به ظاهر ساده» در hashing، در واقع یک data migration است.
این مقاله دربارهی الگوی مهاجرت بود، نه دربارهی اختراع دوبارهی سیستم احراز هویت.
الگوی zero-downtime به این شکل است:
🔹️فرمت جدید برای نوشتنهای جدید
🔸️پشتیبانی از هر دو فرمت برای خواندن
🔹️مهاجرت تدریجی دادههای قدیمی (migrate-on-login یک ترفند عالی است)
ُ🔸️قرار دادن پشت feature flag
🔹️حذف legacy وقتی کار تمام شد
با اجازه دادن به همزیستی فرمت قدیمی و جدید برای مدتی، شما یک مهاجرت کاملاً seamless خواهید داشت.
وقتی مانیتورینگ نشان داد که ۹۹٪ کاربران فعال مهاجرت کردهاند، میتوانید کاربران باقیمانده روی فرمت قدیمی را شناسایی کنید و در تلاش بعدی، آنها را مجبور به reset پسورد کنید.
امیدوارم مفید بوده باشد! ✨
🔖هشتگها:
#Security #PasswordHashing #ZeroDowntime #MigrationPatterns
🚫 Stop Using Boolean Parameters
اجتناب از Flag Argument
مثال بد:
user.SetStatus(true);
وقتی این کد را میخوانیم، باید مکث کنیم و فکر کنیم:
این true یعنی چی؟ Active؟ Verified؟ Deleted؟
این مسئله به نام Flag Argument شناخته میشود.
یعنی پارامتری که مفهومش مبهم است و خواننده را مجبور میکند برای فهمیدن هدف متد، به تعریف آن مراجعه کند.
راهحل: استفاده از متدهای صریح ✅
به جای کد قبلی از:
user.Activate();
استفاده کنید.
مزایا:
✅ هدف واضح است
✅ زبان طبیعی و قابل فهم
✅ نیاز به توضیح یا comment ندارد
💡 نکته مهم:
کد بسیار بیشتر خوانده میشود تا نوشته.
پس هر چه intent کد روشنتر باشد، فهم آن سریعتر و اشتباهات کمتر میشود.
🔖هشتگها:
#CleanCode #FlagArgument #CodeReadability