C# Geeks (.NET)
413 subscribers
152 photos
4 videos
153 links
Download Telegram
چرا شرکت‌های بزرگ یک Reminder Service جداگانه می‌سازند؟

اوایل پروژه همه چیز ساده به نظر می‌رسد.
کاربر یک Task ایجاد می‌کند و برای آن یک زمان یادآوری تعیین می‌کند.
در زمان مقرر هم همان سرویس اصلی یک Notification ارسال می‌کند و تمام.
اما وقتی تعداد کاربران به صدها هزار یا میلیون‌ها نفر می‌رسد، این رویکرد دیگر پاسخگو نیست.
اگر میلیون‌ها Reminder برای ساعت ۹ صبح برنامه‌ریزی شده باشند چه اتفاقی می‌افتد؟
همین‌جاست که مفهوم Reminder Service وارد معماری سیستم می‌شود.
به جای اینکه هر سرویس خودش مسئول زمان‌بندی و ارسال Reminder باشد، یک سرویس مستقل فقط مسئول مدیریت زمان، زمان‌بندی و اجرای Reminderها خواهد بود.
🏗 معماری سنتی
User


Task Service

├── Database

└── Send Reminder

هر سرویس خودش:
• زمان Reminder را ذخیره می‌کند.
• ءScheduler مخصوص خودش را اجرا می‌کند.
• ءNotification ارسال می‌کند.

🏗 معماری با Reminder Service
                User


Task Service

Save Reminder Request


Reminder Service

┌───────────┴────────────┐
▼ ▼
Schedule Engine Delay Queue
│ │
└───────────┬────────────┘

Notification Service

┌───────────┼────────────┐
▼ ▼ ▼
Email SMS Push

تمام سرویس‌ها فقط درخواست ایجاد Reminder را ثبت می‌کنند.
اجرای Reminder کاملاً بر عهده Reminder Service خواهد بود.

مزایا
1️⃣ جداسازی مسئولیت‌ها (Separation of Concerns)

ءBusiness Service فقط منطق کسب‌وکار را مدیریت می‌کند.
زمان‌بندی و اجرای Reminderها کاملاً جدا می‌شود.
2️⃣ مدیریت میلیون‌ها Reminder

ءReminder Service می‌تواند میلیون‌ها Reminder را:
زمان‌بندی کند.
اولویت‌بندی کند.
دسته‌بندی کند.
بدون اینکه سرویس‌های اصلی تحت فشار قرار بگیرند.
3️⃣ ءRetry خودکار

اگر Notification ارسال نشود:
• ءRetry انجام می‌شود.
• ءExponential Backoff اعمال می‌شود.
• در صورت شکست نهایی وارد Dead Letter Queue می‌شود.
4️⃣ مقیاس‌پذیری مستقل

اگر تعداد Reminderها زیاد شود فقط Reminder Service Scale می‌شود.
نیازی نیست کل سیستم Scale شود.
5️⃣ پشتیبانی از انواع Reminder

یک سرویس می‌تواند انواع مختلف Reminder را مدیریت کند:
📧 Email Reminder
📱 SMS Reminder
🔔 Push Notification
📅 Calendar Reminder
💬 Slack Reminder
6️⃣ مدیریت زمان (Time Zone)

یکی از سخت‌ترین قسمت‌های Reminder همین موضوع است.Reminder Service می‌تواند:
• Time Zone کاربران
• Daylight Saving
• Local Time
را به صورت متمرکز مدیریت کند.
7️⃣ قابلیت Delay Scheduling

مثلاً:
Send after 5 minutes
Send tomorrow at 9 AM
Send every Monday
Send after 30 days
Send every month

همه این موارد توسط Reminder Service مدیریت می‌شوند.
معایب
1️⃣ پیچیدگی بیشتر

دیگر با یک Scheduler ساده طرف نیستید.
مباحثی مانند:
• Distributed Scheduling
• Delay Queue
• Cron Engine
• Retry
• Dead Letter Queue
و...
وارد سیستم می‌شوند.
2️⃣ Single Point of Failure

اگر Reminder Service از کار بیفتد،
تمام Reminderهای سیستم متوقف خواهند شد.
بنابراین باید High Availability داشته باشد.
3️⃣ مدیریت دقیق زمان

Clock Drift
Time Zone
Leap Year
Leap Second
Daylight Saving
همگی باید به درستی مدیریت شوند.
4️⃣ جلوگیری از ارسال تکراری

اگر سرویس هنگام ارسال Crash کند،
نباید Reminder دوباره برای کاربر ارسال شود.
بنابراین باید: Idempotency
و Delivery Tracking
وجود داشته باشد.

📋 نکاتی که حتماً باید رعایت شوند
🔸 استفاده از UTC برای ذخیره زمان
🔸 تبدیل Time Zone فقط هنگام نمایش
🔸 استفاده از Delay Queue
🔸 پشتیبانی از Retry
🔸 استفاده از Idempotency Key
🔸 ثبت کامل Audit Log
🔸 پشتیبانی از Cron Expression
🔸 مانیتورینگ Queueها
🔸 قابلیت Cancel کردن Reminder
🔸 قابلیت Reschedule

⭐️ برای بهتر شدن Reminder Service چه کارهایی انجام دهیم؟

Quartz.NET یا Hangfire برای Job Scheduling
RabbitMQ Delay Queue یا Kafka Delay Topic
Outbox Pattern
Distributed Lock
High Availability
Retry Policy
Dead Letter Queue
Monitoring و Alerting
Metrics با OpenTelemetry
Sharding برای Reminderهای حجیم
#مهندس_فکر_کن
قسمت:6️⃣
🎯 مصاحبه‌کننده
فرض کن در ASP.NET Core این کد را نوشته‌ای:
public async Task<string> GetDataAsync()
{
using var client = new HttpClient();

return await client.GetStringAsync("https://api.example.com");
}

این متد روزانه ۱ میلیون بار فراخوانی می‌شود.
🔸️سؤال
این کد چه مشکلی دارد؟
چرا در Production توصیه نمی‌شود؟
راه‌حل استاندارد در ASP.NET Core چیست؟
چیزی که ازت انتظار دارم
اگر فقط بگویی:
"از IHttpClientFactory استفاده می‌کنیم."

کافی نیست.
باید توضیح بدهی:
مشکل دقیق چیست؟
چرا اتفاق می‌افتد؟
Forwarded from thisisnabi.dev [Farsi] (Nabi Karampour)
اگر قرار باشه دوباره درگیر ساختار های دست و پاگیر در فرایند توسعه نرم افزار بشیم باز رسیدن به خروجی زمانبر خواهد بود.
و این دوباره تمرکز ما رو از نیت اصلی دور میکنه.

چند روز پیش با یک پروداکت منیجر بصورت تمرینی روی یک PRD کار می کردم و با هم pair شده بودیم، یک داشبورد کامل رو با درنظر گرفتن اکثر جزئیات بیزینسی در 4.5 ساعت کار آماده کردیم.

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

برای همین بشخصه تمرکزم روی تغییر مدل کاری تیم و چیدن ساختاری برای رسیدن سریعتر به خروجی هست.

حتی یک موضوعی که این روزها فکرم رو درگیر کرده اینه که دیگه اینقدر راحت مثل قبل نیرو در اختیارمون قرار نمیدن،
دیدگاه رفته به سمتی که چرا یه بیزینس میلیارد دلاری توی سیلیکون ولی با 10 نفر پیش میره، شما هم برید به اون سمت، کارهای دست و پاگیرتون رو با AI پیش ببرید.

این تغییر نشونه بیکارشدن ما نیست ولی بیزینس چی براش کمتر هزینه داشته باشه میره به اون سمت، تا بوده همین بوده.
شما سعی کنید مسبب این کاهش هزینه باشید.
(#باور_غلط_یا_واقعیت؟)
باور غلط

«هرچه تعداد Design Patternهایی که در پروژه استفاده کنیم بیشتر باشد، کیفیت طراحی هم بالاتر است.»
واقعیت

ءDesign Patternها قرار نیست کیفیت طراحی را ایجاد کنند.آن‌ها قرار است یک مسئله تکرارشونده را با یک راه‌حل شناخته‌شده حل کنند.
اگر مسئله وجود نداشته باشد، Pattern هم ارزشی ایجاد نمی‌کند.
بارها دیده‌ام پروژه‌هایی که پر از Factory، Strategy، Decorator، Mediator و... هستند.
اما تغییر دادن یک قابلیت ساده در آن‌ها، ساعت‌ها زمان می‌برد.نه به خاطر پیچیدگی مسئله...
بلکه به خاطر پیچیدگی‌ای که خودمان ساخته‌ایم.
از طرف دیگر، پروژه‌هایی را هم دیده‌ام که شاید حتی یک Pattern را نتوان روی آن‌ها نام‌گذاری کرد.
اما خوانا بودند.
تست‌پذیر بودند.
و تغییر دادنشان ساده بود.
نکته مهم اینجاست: Design Pattern، هدف نیست. فقط یک ابزار است.
همان‌طور که چکش برای هر چیزی ساخته نشده، Design Pattern هم برای هر کدی مناسب نیست.
مهندسان باتجربه معمولاً هنگام طراحی از خودشان نمی‌پرسند:
«کدام Pattern را اینجا استفاده کنم؟»
آن‌ها می‌پرسند: «اینجا چه مشکلی دارم؟»
اگر Pattern آن مشکل را حل کند، از آن استفاده می‌کنند.
اگر نه، ساده‌ترین راه‌حل را انتخاب می‌کنند.
💡 جمع‌بندی

کدی که هیچ Pattern مشهوری ندارد اما به‌راحتی قابل فهم، تغییر و نگهداری است،
از کدی که فقط برای نمایش دانش الگوهای طراحی نوشته شده،ارزش مهندسی بیشتری دارد.چون در مهندسی نرم‌افزار،سادگی یک مزیت است؛ نه نشانه کم‌تجربگی.
🧅 معماری Onion در برابر 🧼 معماری Clean؛ تفاوت در چیست؟

هر دو معماری حول یک ایده‌ی مشترک شکل گرفته‌اند:
• جهت وابستگی‌ها به سمت مرکز است.
• هسته‌ی سیستم شامل موجودیت‌های دامنه (Domain Entities) و قوانین کسب‌وکار (Business Rules) است.
• هسته‌ی سیستم باید از هرگونه وابستگی به مسائل خارجی مستقل باشد.
ایده‌ی اصلی در اینجا کنترل Coupling (وابستگی) است.
موجودیت‌های دامنه و قوانین کسب‌وکار در مرکز سیستم قرار دارند. آن‌ها به هیچ چیز وابسته نیستند و این موضوع ارزشمند است، زیرا باعث می‌شود پایدار باقی بمانند. اجزای اطراف Domain ممکن است تغییر کنند، اما این تغییرات نباید روی موجودیت‌های دامنه یا قوانین کسب‌وکار تأثیر بگذارند.
به خاطر داشته باشید که Coupling به میزان وابستگی متقابل بین ماژول‌ها و کامپوننت‌های یک سیستم گفته می‌شود.
هر دو معماری Clean و Onion تلاش می‌کنند به اهداف یکسانی برسند، اما با رویکردی کمی متفاوت.
در هر دو، لایه‌ی Domain وجود دارد.
در Clean Architecture، هسته شامل Use Caseها است، در حالی که در Onion Architecture، از Domain Services و Application Services استفاده می‌شود.
در بیرونی‌ترین لایه نیز دغدغه‌های مربوط به Infrastructure قرار می‌گیرند.
هر تفاوتی که میان این دو مشاهده می‌کنید، زمانی که نوبت به پیاده‌سازی آن‌ها در یک پروژه واقعی می‌رسد، به سرعت کمرنگ می‌شود؛ که این موضوع کاملاً طبیعی است، زیرا هر دو در حال پیاده‌سازی مجموعه‌ای از اصول یکسان هستند.
عامل مهم دیگری که باید به آن توجه کنید Cohesion (انسجام) است. Cohesion به میزان ارتباط و هماهنگی اجزای داخلی یک ماژول (کلاس، تابع، کامپوننت و...) برای انجام یک هدف مشخص و واحد گفته می‌شود.
در واقع، همین مفهوم مهم‌ترین نیروی محرک Vertical Slice Architecture محسوب می‌شود.
#فرهنگ_مهندسی (Engineering Culture)
یکی از نشانه‌های یک تیم مهندسی بالغ، این نیست که کمتر خطا می‌کند.
این است که از خطاها چیزی پنهان نمی‌کند.
در بعضی تیم‌ها، وقتی Incident رخ می‌دهد، اولین سؤال این است:
«کارِ چه کسی بود؟»
اما در تیم‌های مهندسی بالغ، سؤال فرق می‌کند:
«چطور ممکن است دوباره این اتفاق نیفتد؟»
این تفاوت، کوچک به نظر می‌رسد.
اما فرهنگ تیم را از ریشه تغییر می‌دهد.
وقتی افراد از سرزنش شدن بترسند،
مشکلات را دیرتر گزارش می‌کنند.
ریسک‌ها را پنهان می‌کنند.
و گاهی ترجیح می‌دهند باگ را موقتاً دور بزنند تا اینکه درباره آن صحبت کنند.
اما وقتی بدانند هدف، پیدا کردن مقصر نیست،
بلکه پیدا کردن علت است،
اطلاعات سریع‌تر به اشتراک گذاشته می‌شود.
و تیم، با هر Incident قوی‌تر از قبل می‌شود.
به همین دلیل است که Blameless Postmortem فقط یک جلسه بعد از Incident نیست.
یک طرز فکر است.
طرز فکری که می‌گوید:
«انسان‌ها اشتباه می‌کنند؛ وظیفه سیستم این است که اجازه ندهد یک اشتباه کوچک، به یک فاجعه بزرگ تبدیل شود.»
فرهنگ مهندسی از جایی شروع می‌شود که اعضای تیم،
برای گفتن حقیقت، احساس امنیت کنند.
چون تیمی که از اشتباهاتش یاد می‌گیرد،
خیلی سریع‌تر از تیمی رشد می‌کند که فقط سعی می‌کند اشتباهاتش را پنهان کند.
🚨 قابلیت جدید field در C# 14 همیشه هم بی‌خطر نیست!

یکی از جذاب‌ترین قابلیت‌های C# 14 معرفی کلیدواژه field است.
به کمک آن دیگر لازم نیست برای هر Property یک Backing Field جداگانه تعریف کنید.
قبلاً می‌نوشتیم:
private decimal _price;

public decimal Price
{
get => _price;
set => _price = value;
}

حالا می‌توانیم بنویسیم:
public decimal Price
{
get;
set => field = value;
}

کد تمیزتر، خواناتر و Boilerplate کمتر...
اما یک نکته مهم وجود دارد که خیلی‌ها به آن توجه نمی‌کنند. 👇
⚠️ مشکل از کجا شروع می‌شود؟

وقتی از field استفاده می‌کنید، دیگر فیلدی به نام _price وجود ندارد.
کامپایلر خودش یک Backing Field مخفی ایجاد می‌کند.
چیزی شبیه:
<Price>k__BackingField

این یعنی اگر جایی از پروژه مستقیماً به _price وابسته باشد...
ممکن است برنامه بدون هیچ خطای کامپایل، در Runtime دچار مشکل شود.

🔥 یکی از رایج‌ترین قربانی‌ها: EF Core

فرض کنید قبلاً این Mapping را نوشته‌اید:
builder.Property(x => x.Quantity)
.HasField("_quantity");

بعد تصمیم می‌گیرید از قابلیت جدید C# 14 استفاده کنید.
public int Quantity
{
get;
set => field = value;
}

نتیجه؟
هنگام اجرای برنامه با خطای:
InvalidOperationException

روبه‌رو خواهید شد.
چون دیگر فیلدی به نام _quantity وجود ندارد.
⚠️ ءReflection هم ممکن است از کار بیفتد
کدهایی مانند:

GetField("_quantity")

یا
GetRuntimeField(...)

بعد از Refactor مقدار null برمی‌گردانند.
و معمولاً چند خط بعد...
به یک NullReferenceException ختم می‌شوند.
قبل از استفاده از field این موارد را بررسی کنید

🔹 تمام HasField(...)های پروژه
🔹 ءAttributeهای BackingField
🔹 استفاده از Reflection
🔹 ءMapperهای سفارشی
🔹 ءSerializerهایی که روی Fieldها کار می‌کنند
🔹 ءUnit Testهایی که به نام Field وابسته‌اند
💡 نکته مهم

استفاده از field فقط یک Refactor ظاهری نیست.
اگر زیرساخت پروژه به نام Backing Field وابسته باشد، در واقع دارید Contract داخلی برنامه را تغییر می‌دهید.
و این دسته از خطاها معمولاً نه توسط کامپایلر شناسایی می‌شوند و نه توسط بسیاری از Unit Testها؛ بلکه اولین بار در محیط Production خودشان را نشان می‌دهند.
🎯 جمع‌بندی

قابلیت field در C# 14 یکی از بهترین امکانات جدید زبان است و باعث کاهش Boilerplate و خواناتر شدن کد می‌شود.
اما قبل از مهاجرت، مطمئن شوید پروژه شما به نام Backing Fieldها وابسته نیست؛ در غیر این صورت، یک Refactor ساده می‌تواند به خطاهای Runtime، شکست Mappingهای EF Core و مشکلات Reflection منجر شود.

🔗Source
🔖 هشتگ‌ها:
#dotnet #csharp14 #EFCore #Reflection #Runtime #DotNetTips
🖼 چرا شرکت‌های بزرگ یک Media Service جداگانه می‌سازند؟

اوایل پروژه همه چیز ساده است.
کاربر یک تصویر آپلود می‌کند، فایل داخل wwwroot ذخیره می‌شود و مسیر آن داخل دیتابیس قرار می‌گیرد.
اما وقتی سیستم بزرگ‌تر می‌شود، ناگهان هر سرویس شروع می‌کند به مدیریت فایل‌ها، آپلود تصاویر، Resize کردن، ساخت Thumbnail، حذف فایل‌ها، CDN، Storage و ده‌ها مسئولیت دیگر.
همین‌جاست که مفهوم Media Service وارد می‌شود.
به جای اینکه هر سرویس خودش مسئول مدیریت فایل‌ها باشد، یک سرویس مرکزی فقط روی مدیریت Media تمرکز می‌کند.

🏗 معماری با Media Service
                Upload File

Media Service

Object Storage (S3, MinIO)

CDN / Image Cache

┌─────────┼─────────┐
↓ ↓ ↓
Service A Service B Service C

تمام سرویس‌ها فقط از Media Service درخواست Upload یا Download می‌کنند و دیگر درگیر جزئیات ذخیره‌سازی فایل نیستند.

مزایا
1️⃣ جداسازی مسئولیت‌ها (Single Responsibility)

ءBusiness Serviceها فقط روی منطق کسب‌وکار تمرکز می‌کنند. Media Service فقط فایل مدیریت می‌کند.
2️⃣ یکپارچگی Storage

تمام فایل‌ها در یک محل ذخیره می‌شوند.
فرقی ندارد از:
• Local Storage
• MinIO
• Amazon S3
• Azure Blob Storage
• Google Cloud Storage
استفاده کنید.
تمام سرویس‌ها فقط با Media Service صحبت می‌کنند.
3️⃣ حذف کدهای تکراری

دیگر لازم نیست هر سرویس جداگانه پیاده‌سازی کند:
• Upload Endpoint
• Delete File
• Resize Image
• Compress Image
• Generate Thumbnail
• Virus Scan
4️⃣ توسعه‌پذیری بیشتر

اگر امروز از Local Storage استفاده می‌کنید و فردا بخواهید به S3 مهاجرت کنید،
فقط Media Service تغییر می‌کند.
هیچ Business Serviceای نیاز به تغییر ندارد.
5️⃣ مدیریت ساده‌تر فایل‌ها

تمام عملیات مربوط به فایل در یک نقطه انجام می‌شود:
• Versioning
• Metadata
• Tags
• File Ownership
• Expiration
• Access Policy
6️⃣ ءPerformance بهتر

ءMedia Service می‌تواند:
• Image Resize
• Compression
• Lazy Loading
• CDN Integration
• Cache
را به صورت متمرکز مدیریت کند.

معایب
1️⃣ Single Point Of Failure

اگر Media Service از دسترس خارج شود،
آپلود و دانلود فایل‌ها نیز متوقف می‌شود.
2️⃣ پیچیدگی بیشتر

دیگر فقط ذخیره فایل مطرح نیست.
موضوعاتی مانند:
• CDN
• Object Storage
• Streaming
• Chunk Upload
• Signed URL
• Image Processing
نیز وارد سیستم می‌شوند.
3️⃣ هزینه بیشتر

نگهداری Storage، CDN و Backup هزینه دارد.
4️⃣ مدیریت امنیت

دسترسی مستقیم کاربران به فایل‌ها باید کنترل شود.خصوصاً برای فایل‌های Private.

📋 نکاتی که حتماً باید رعایت شوند

🔸 فایل‌ها داخل Database ذخیره نشوند (به‌جز موارد خاص)
🔸 از Object Storage استفاده شود.
🔸 فایل‌های Private با Signed URL ارائه شوند.
🔸 محدودیت حجم فایل اعمال شود.
🔸 ءMIME Type اعتبارسنجی شود.
🔸 نام فایل‌ها به GUID یا Hash تبدیل شوند.
🔸 ءThumbnailها به صورت Async تولید شوند.
🔸 فایل‌های بلااستفاده پاک‌سازی شوند.
🔸 از CDN برای فایل‌های عمومی استفاده شود.
🔸 تمام عملیات Upload و Delete لاگ شوند.

⭐️ برای بهتر شدن Media Service چه کارهایی انجام دهیم؟

Chunk Upload برای فایل‌های بزرگ
Resume Upload
Background Image Processing
Automatic Compression
Automatic Thumbnail Generation
Video Transcoding
Signed URL
CDN Integration
Metadata Search
Duplicate Detection (Hash)
Virus Scanning
Versioning
Lifecycle Management
Backup & Replication
💡 مهم‌ترین نکته
ءMedia Service فقط یک سرویس Upload فایل نیست.
در سیستم‌های بزرگ، این سرویس مسئول مدیریت کل چرخه‌ی عمر فایل‌ها است؛ از لحظه‌ی آپلود تا حذف، آرشیو، Cache، امنیت، پردازش تصویر و توزیع از طریق CDN.
هرچه زودتر مدیریت فایل‌ها را از Business Serviceها جدا کنید، توسعه سیستم ساده‌تر، مقیاس‌پذیری بیشتر و نگهداری زیرساخت بسیار آسان‌تر خواهد شد.

🔖 هشتگ‌ها:
#mediaservice #objectstorage #minio #amazons3
#فرمون_دادن
بعضی وقت‌ها فکر می‌کنیم ارزش یک مهندس را تعداد تصمیم‌هایی که می‌گیرد مشخص می‌کند.
اما واقعیت این است که...
خیلی از مهندسان باتجربه، بیشتر از اینکه تصمیم بگیرند،
تصمیم نگرفتن را بلدند.
آن‌ها هر ایده‌ای را تبدیل به Feature نمی‌کنند.
هر تکنولوژی جدیدی را وارد پروژه نمی‌کنند.
هر مشکلی را هم فوراً به Refactoring ربط نمی‌دهند.
چون یک چیز را خوب می‌دانند.
هر تصمیمی که امروز می‌گیری،
از فردا به بعد باید هزینه نگهداری‌اش را هم پرداخت کنی.
هر کلاس جدید...
هر سرویس جدید...
هر Dependency جدید...
هر Feature جدید...
تا روزی که پروژه زنده است، از تو هزینه می‌گیرد.
به همین دلیل، بلوغ مهندسی فقط در این نیست که بدانی چه چیزی را اضافه کنی.
بلوغ واقعی این است که بدانی به چه چیزی باید «نه» بگویی.
چون در نهایت،
پروژه‌ها بیشتر از اینکه زیر بار کمبود قابلیت‌ها خم شوند،
زیر بار تصمیم‌هایی خم می‌شوند که هیچ‌وقت لازم نبود گرفته شوند.
🔍 وقتی یک تراکنش وسط مسیر Fail می‌شود، واقعاً از کجا شروع به Debug می‌کنید؟
فرض کنید یک کاربر روی دکمه پرداخت کلیک می‌کند. چند ثانیه بعد...
پرداخت ناموفق می‌شود.
حالا سؤال اینجاست:
مشکل دقیقاً کجا رخ داده است؟
آیا API Gateway درخواست را دریافت نکرده؟
آیا Validation در Command Handler شکست خورده؟
آیا داده داخل Database ذخیره نشده؟
آیا Outbox پردازش نشده؟
آیا پیام وارد RabbitMQ نشده؟
یا Consumer پیام را دریافت کرده ولی پردازش آن Fail شده؟
اگر هر سرویس فقط لاگ خودش را داشته باشد...
عملاً وارد یک کابوس خواهید شد.
😵 سناریوی رایج

فرض کنید سیستم شما از این بخش‌ها تشکیل شده است:
Client


API Gateway (Ocelot)


Payment Service


Command Handler


Database + Outbox


Background Job


RabbitMQ


Notification Service

حالا برای پیدا کردن علت خطا باید:
🔹 لاگ Gateway را باز کنید.
🔹 بعد لاگ Payment Service را بررسی کنید.
🔹 بعد لاگ Background Service را.
🔹 بعد لاگ RabbitMQ Consumer را.
🔹 بعد لاگ Notification Service را.
هر کدام روی یک Instance جدا... با Timestampهای مختلف... و فقط امیدوار باشید که ساعت همه سرورها Sync باشد.

📌راه‌حل؟
ءCentralized Logging + Correlation ID + OpenTelemetry
🎯 تجربه واقعی در Hub پرداخت Dr. Link
در هاب پرداخت Dr. Link تعداد زیادی سرویس مستقل وجود دارد.
پرداخت فقط یک درخواست HTTP نیست.
بعد از ثبت درخواست، چندین اتفاق پشت سر هم رخ می‌دهد:
ذخیره اطلاعات
اجرای Business Ruleها
ثبت Domain Event
ذخیره در Outbox
ءPublish شدن پیام توسط MassTransit
پردازش توسط سرویس‌های دیگر
اگر Correlation نداشته باشیم...
ردیابی مسیر تقریباً غیرممکن می‌شود.
🆔 همه چیز از Correlation ID شروع می‌شود
به محض اینکه درخواست وارد Ocelot API Gateway می‌شود...
یک Correlation ID تولید می‌شود.
مثلاً:
a17d59d8-78d2-4b1f-bd89-40d3f52b44f3

این شناسه همراه درخواست به تمام سرویس‌ها ارسال می‌شود.
X-Correlation-Id:
a17d59d8-78d2-4b1f-bd89-40d3f52b44f3

از این لحظه...
تمام لاگ‌های مربوط به این درخواست همین شناسه را خواهند داشت.
🌐 اما داستان فقط HTTP نیست...

قسمت سخت ماجرا زمانی شروع می‌شود که سیستم وارد دنیای Asynchronous Messaging می‌شود.
در همین نقطه رد درخواست گم می‌شود.
📨 ءCorrelation ID داخل پیام هم قرار می‌گیرد
هنگام Publish کردن پیام توسط MassTransit همان Correlation ID داخل Envelope پیام ذخیره می‌شود.
یعنی پیام چیزی شبیه این خواهد داشت:
CorrelationId

a17d59d8-78d2-4b1f-bd89-40d3f52b44f3

در نتیجه...
وقتی Consumer پیام را دریافت می‌کند...
همان شناسه هنوز همراه پیام است.
هیچ بخشی از مسیر گم نمی‌شود.
📝 ءSerilog هم به صورت خودکار این شناسه را وارد Logها می‌کند
به کمک یک Serilog Enricher
ءCorrelation ID مستقیماً از:
HTTP Header
یا
MassTransit Message
خوانده می‌شود.
در نتیجه دیگر لازم نیست بنویسیم:
Log.Information(
"Payment {CorrelationId}",
correlationId);

تمام Logها به صورت خودکار این مقدار را خواهند داشت.
⚙️ نقش MediatR Pipeline Behavior

قبلاً برای Cross-Cutting Concernها از Pipeline Behavior استفاده کرده بودیم.
حالا همان Pipeline یک مسئولیت جدید هم دارد.
در ابتدای اجرای هر Command:
یک Logging Scope باز می‌کند.
BeginScope()

CorrelationId

از این لحظه...
هر لاگی که داخل:
Repository
Domain Service
Application Service
EF Core
یا هر بخش دیگری ثبت شود...
به صورت خودکار همین Correlation ID را خواهد داشت.
بدون اینکه حتی یک بار آن را دستی پاس بدهیم.
📊 نتیجه نهایی

وقتی یک تراکنش Fail می‌شود...
دیگر لازم نیست:
وارد لاگ Gateway شوید.
بعد Payment Service.
بعد Background Job.
بعد RabbitMQ.
بعد Notification Service.
فقط کافی است در Seq این شناسه را جستجو کنید:
a17d59d8-78d2-4b1f-bd89-40d3f52b44f3

و کل مسیر درخواست را ببینید.
از اولین HTTP Request...
تا آخرین Consumer...
کاملاً مرتب و به ترتیب زمانی.
🔭 اگر بخواهیم یک قدم جلوتر برویم...

ءCorrelation ID فقط برای لاگ‌ها نیست.
اگر از OpenTelemetry استفاده کنید...
همین شناسه تبدیل به یک Distributed Trace می‌شود.
در آن صورت می‌توانید ببینید:
⏱️ هر سرویس چقدر زمان صرف کرده است.
📡 کدام درخواست به کدام سرویس ارسال شده است.
🐢 گلوگاه سیستم دقیقاً کجاست.
💥 کدام Span با خطا مواجه شده است.
یعنی علاوه بر مشاهده لاگ‌ها، کل سفر یک درخواست را به‌صورت تصویری دنبال می‌کنید.
💡 مهم‌ترین نکته

در سیستم‌های Monolith معمولاً Debug کردن فقط با یک فایل Log انجام می‌شود.
اما در معماری‌های Microservices یا حتی Modular Monolith که از Message Broker، Outbox Pattern و Background Processing استفاده می‌کنند، بدون Correlation ID عملاً هر سرویس جزیره‌ای جداگانه است.
ءCentralized Logging، Correlation ID و OpenTelemetry فقط ابزارهای لاگ‌گیری نیستند؛ آن‌ها ستون فقرات Observability سیستم هستند. هرچه سیستم بزرگ‌تر و توزیع‌شده‌تر شود، ارزش این سه ابزار بیشتر نمایان می‌شود، چون در زمان بروز Incident، تفاوت بین چند دقیقه و چند ساعت عیب‌یابی را رقم می‌زنند.

🔖 هشتگ‌ها:
#aspnetcore #microservices #serilog #opentelemetry #observability #logging
#روایت_تجربه (Storytelling)
چند وقت پیش، یکی از مهندس‌های ارشد در یک کنفرانس جمله‌ای گفت که توجهم را جلب کرد.
گفت:
«هر وقت وارد یک کدبیس جدید می‌شوم، قبل از اینکه اولین خط کد را تغییر دهم، سعی می‌کنم بفهمم تیم از تغییر دادن کدام قسمت می‌ترسد.»
ابتدا فکر کردم منظورش کیفیت کد است.
اما ادامه داد:
«در همه پروژه‌ها یک یا چند فایل وجود دارد که وقتی اسمشان می‌آید، همه می‌گویند:
لطفاً تا جای ممکن به آن دست نزن...»
نه چون آن فایل مهم‌ترین بخش سیستم است.
بلکه چون هیچ‌کس مطمئن نیست بعد از تغییرش چه اتفاقی می‌افتد.
او می‌گفت:
«همان فایل، معمولاً بیشترین بدهی فنی پروژه را دارد؛ نه لزوماً بزرگ‌ترین فایل.»
این حرف برایم جالب بود.
چون ما معمولاً بدهی فنی را با تعداد خطوط کد، پیچیدگی یا قدمت پروژه اندازه می‌گیریم.
اما شاید معیار بهتری وجود داشته باشد.
ترس تیم از تغییر.
اگر تیم از تغییر دادن بخشی از سیستم واهمه داشته باشد،
احتمالاً آن بخش، مدت‌هاست که از کنترل خارج شده است.
از آن روز، هر وقت درباره سلامت یک کدبیس صحبت می‌شود، کمتر به تعداد کلاس‌ها یا متدها فکر می‌کنم.
بیشتر به این فکر می‌کنم که:
«اگر فردا یک تغییر کوچک در این قسمت لازم باشد، آیا تیم با اطمینان آن را انجام می‌دهد؟»
چون در مهندسی نرم‌افزار،
گاهی خطرناک‌ترین بخش سیستم،
بخشی نیست که بیشترین باگ را دارد.
بخشی است که همه از تغییر دادنش می‌ترسند.
این سؤال را چند وقت پیش دیدم و به نظرم سؤال جالبی بود.
سؤال این بود:
مهندس‌های نرم‌افزاری که حقوق‌های بسیار بالایی می‌گیرند، دقیقاً چه نوع مسائلی را حل می‌کنند؟

برداشت اولیه این سؤال این است که انگار اگر در یک حوزه خاص کار کنید، خودبه‌خود درآمد بالایی خواهید داشت.
اما واقعیت کمی متفاوت است.
شرکت‌های مختلف، کشورهای مختلف و حتی دو استارتاپ در یک صنعت، مدل‌های پرداخت کاملاً متفاوتی دارند.
یک استارتاپ تازه‌تأسیس ممکن است با بودجه محدود حقوق پرداخت کند، در حالی که یک شرکت بزرگ فناوری برای همان نقش چند برابر بیشتر هزینه کند.
بنابراین ارتباط مستقیم بین نوع مسئله و میزان حقوق همیشه وجود ندارد.
در واقع این رابطه، یک رابطه علت و معلولی ساده نیست.
🎯 چیزی که واقعاً ارزش شما را تعیین می‌کند چیست؟

بر اساس تجربه‌ای که در شرکت‌های مختلف دیده‌ام، مهندس‌هایی که بیشترین ارزش را ایجاد می‌کنند معمولاً در یکی از این سه دسته قرار می‌گیرند:
🚀 1️⃣ حل مسائل با ارزش تجاری بالا در حوزه‌های تخصصی
آن‌ها روی مسائلی کار می‌کنند که شاید افراد کمی توانایی حلشان را داشته باشند، اما تأثیر بسیار بزرگی روی کسب‌وکار دارند.
مثلاً:
بهینه‌سازی یک سیستم پرداخت که روزانه میلیون‌ها تراکنش پردازش می‌کند.
طراحی زیرساختی که هزینه سرورها را ۳۰٪ کاهش می‌دهد.
افزایش Availability یک سرویس حیاتی.
🤝 2️⃣ تبدیل شدن به یک «Multiplier»
این افراد فقط خودشان سریع کدنویسی نمی‌کنند.
آن‌ها باعث می‌شوند کل تیم سریع‌تر و بهتر کار کند.
مثلاً:
طراحی معماری مناسب
ایجاد ابزارهای داخلی
ءMentor کردن اعضای تیم
بهبود فرآیندهای توسعه
حذف گلوگاه‌های فنی
گاهی ارزش واقعی یک مهندس، در تعداد خط کدی که می‌نویسد نیست؛ بلکه در تعداد افرادی است که بهره‌وری آن‌ها را افزایش می‌دهد.
📈 3️⃣ ایجاد ارزش پایدار در بخش‌های مختلف سیستم
برخی مهندس‌ها فقط روی یک Feature کار نمی‌کنند.
آن‌ها به صورت مداوم روی بخش‌های مختلف سیستم اثر مثبت می‌گذارند.
برای مثال:
بهبود Performance
افزایش Security
کاهش هزینه‌های زیرساخت
افزایش Reliability
ساده‌تر کردن فرآیند توسعه
💡 اگر بخواهیم خیلی ساده بیان کنیم...
می‌توان گفت:
حقوق ≈ ارزشی که ایجاد می‌کنید × گستره تأثیرگذاری شما

هرچه خروجی شما ارزشمندتر باشد و افراد یا بخش‌های بیشتری از آن بهره ببرند، احتمال دریافت مسئولیت‌ها و جبران خدمات بالاتر نیز بیشتر خواهد بود.
⚠️ آیا متخصص شدن در یک حوزه بسیار خاص بهترین راه است؟

نه همیشه.
داشتن تخصص عمیق در یک حوزه خاص می‌تواند بسیار ارزشمند باشد.
اما اگر تمام استراتژی شغلی خود را فقط بر اساس یک مهارت بسیار محدود و خاص بنا کنید، ریسک بالایی دارد.
ممکن است آن مهارت در بازار امروز بسیار پرتقاضا باشد، اما چند سال بعد شرایط کاملاً تغییر کند.
🎯 پس روی چه چیزی سرمایه‌گذاری کنیم؟
اگر هدف رشد حرفه‌ای است، بهتر است روی مهارت‌هایی تمرکز کنید که در هر پروژه و هر سازمانی ارزش ایجاد می‌کنند.
مانند:
توانایی حل مسئله
طراحی معماری
تصمیم‌گیری مهندسی
درک Trade-offها
ارتباط مؤثر با تیم
ایجاد ارزش برای کسب‌وکار، نه فقط نوشتن کد
💬 مهم‌ترین نکته
بسیاری از افراد فکر می‌کنند:
مهندس‌هایی که بیشترین حقوق را می‌گیرند، فقط کدنویس‌های بهتری هستند.

اما در عمل، تفاوت اصلی آن‌ها معمولاً در میزان ارزشی است که برای کسب‌وکار ایجاد می‌کنند، نه صرفاً در تعداد خطوط کدی که می‌نویسند.
در نهایت، شرکت‌ها برای «کد» پول پرداخت نمی‌کنند؛ برای حل مسئله و ایجاد ارزش هزینه می‌کنند.
در طول سال‌های گذشته، سیستم اعتبارسنجی مدل (Model Validation) توکار در اکوسیستم دات‌نت که عمدتاً بر پایه‌ی فضای نام System.ComponentModel.DataAnnotations استوار است، ساختاری کاملاً همگام (Synchronous) داشت. ⚙️
این طراحی سنتی، پیاده‌سازی قواعد اعتبارسنجی پیچیده‌ای را که به عملیات‌های ورودی/خروجی (I/O-bound) وابسته بودند با چالش مواجه می‌کرد؛ سناریوهایی مانند:
بررسی منحصربه‌فرد بودن نام کاربری در پایگاه داده
استعلام وضعیت یک شناسه از طریق Web API خارجی
سنجش موجودی انبار
توسعه‌دهندگان برای پیاده‌سازی این زیرساخت‌ها ناچار بودند یا به ابزارهای شخص ثالث مانند کتابخانه محبوب FluentValidation روی آورند، یا متدهای ناهمگام را به صورت ناامن و مسدودکننده (Thread Blocking) با استفاده از امضا‌هایی مانند .Result یا .Wait() فراخوانی کنند. ⚠️
این رویکرد نامناسب، مستقیماً ریسک بروز گلوگاه در پایگاه ریسمان‌ها (Thread Pool Starvation) را افزایش می‌داد. 📉
در پی درخواست‌های مکرر جامعه توسعه‌دهندگان، مایکروسافت بالاخره در NET 11 Preview 6. زیرساخت اعتبارسنجی ناهمگام را به صورت بومی به هسته فریم‌ورک اضافه کرد تا به یکی از پررأی‌ترین درخواست‌های گیت‌هاب پاسخ دهد. 🎉
🆕 معماری و قابلیت‌های جدید در NET 11.

این تغییرات زیرساختی، معماری اعتبارسنجی دات‌نت را متحول کرده است. 🔥
در این مکانیزم جدید، سه افزونه اساسی به فضای نام
System.ComponentModel.DataAnnotations
اضافه شده است که به توسعه‌دهندگان اجازه می‌دهد منطق ناهمگام خود را بدون مسدود کردن ریسمان‌ها پیاده‌سازی کنند:
🟢 کلاس انتزاعی AsyncValidationAttribute
این کلاس پایه جدید، جایگزین مستقیم ValidationAttribute برای سناریوهای ناهمگام است و امکان نوشتن اتریبیوت‌های سفارشی را با بازنویسی (Override) متد IsValidAsync فراهم می‌کند.
🟢 اینترفیس IAsyncValidatableObject
همتای ناهمگام اینترفیس شناخته‌شده‌ی IValidatableObject که به خودِ مدل یا DTO اجازه می‌دهد منطق اعتبارسنجی درونی‌اش را به صورت اسنک (Async) پیاده‌سازی کند.
🟢 متدهای نوین در کلاس static اعتبارسنجی (Validator)
معرفی متدهای جدید از جمله:
ValidateObjectAsync
TryValidateObjectAsync
ValidatePropertyAsync
ValidateValueAsync
جهت اجرای غیرمسدودکننده عملیات سنجش صحت داده‌ها. 🚀

🧩 پیاده‌سازی عملی اتریبیوت اعتبارسنجی ناهمگام

در نمونه کد زیر، شیوه تعریف یک قانون اعتبارسنجی ناهمگام جهت بررسی منحصربه‌فرد بودن نام کاربری به ساده‌ترین شکل ممکن ارائه شده است.
متد سنتی IsValid به صورت پیش‌فرض برای سازگاری عقب‌رو (Backward Compatibility) مقدار ValidationResult.Success را برمی‌گرداند و منطق اصلی در IsValidAsync به همراه پشتیبانی از CancellationToken پیاده‌سازی می‌شود.
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;

public sealed class UniqueUserNameAttribute : AsyncValidationAttribute
{
// سازگاری با خط لوله‌های همگام قدیمی
protected override ValidationResult? IsValid(object? value, ValidationContext context)
=> ValidationResult.Success;

// پیاده‌سازی اصلی برای خط لوله ناهمگام دات‌نت ۱۱
protected override async Task<ValidationResult?> IsValidAsync(
object? value, ValidationContext context, CancellationToken cancellationToken)
{
// دریافت مستقیم سرویس‌ها از کانتینر DI با استفاده از Context موجود
var users = context.GetRequiredService<IUserStore>();

bool isTaken = await users.ExistsAsync((string?)value, cancellationToken);

return isTaken
? new ValidationResult("این نام کاربری قبلاً توسط کاربر دیگری انتخاب شده است.")
: ValidationResult.Success;
}
}

public class RegistrationRequest
{
[Required]
[UniqueUserName]
public string UserName { get; set; } = "";
}

🚀 اجرای اعتبارسنجی ناهمگام
جهت فراخوانی و اعتبارسنجی ناهمگام این مدل در خط لوله‌های دستی یا خارج از لایه کنترلر، موتور Validator متدهای الحاقی جدید خود را ارائه می‌دهد.
// اجرای فرآیند اعتبارسنجی ناهمگام بدون Block کردن Thread
var context = new ValidationContext(model, serviceProvider, items: null);

await Validator.ValidateObjectAsync(
model,
context,
validateAllProperties: true);