C# Geeks (.NET)
549 subscribers
157 photos
4 videos
177 links
Download Telegram
📌 200+ سوال مصاحبه واقعی (بخش 3️⃣)

[ BUCKET 2 - ASP.NET Core AND EF Core ]

🌐 فصل 25 — انواع پروژه‌های وب در ASP.NET Core

📌 76. انواع اصلی پروژه‌های وب در ASP.NET Core چیستند؟
📌 77. چه زمانی Minimal APIs را به‌جای یک پروژه‌ی API مبتنی بر Controller انتخاب می‌کنید؟

🏠 فصل 26 — Application Host

⚙️ 78. ءWebApplicationBuilder چیست و چگونه پیکربندی Host و Application را ساده‌تر می‌کند؟
🔄 79. ءIHostedService چیست و یک کاربرد معمول آن چیست؟
🛑 80. ءIHost.StopAsync از چه مکانیزم‌هایی برای ارسال سیگنال Graceful Shutdown استفاده می‌کند؟

🎮 فصل 27 — Controllers

🔗 81. ءModel Binding چیست و در ASP.NET Core Controllers چگونه کار می‌کند؟
📥 82. تفاوت بین [FromBody]، [FromQuery]، [FromRoute] و [FromForm] چیست؟
🎯 83. هدف IActionResult چیست و چرا ممکن است آن را به یک Concrete Return Type ترجیح دهیم؟
♻️ 84. چرخه‌ی حیات یک Controller در ASP.NET Core چگونه است؟ چه زمانی ساخته و چه زمانی Dispose می‌شود؟
🚨 85. چگونه می‌توان Exceptionها را به‌صورت سراسری برای تمام Controllerها مدیریت کرد؟

⚡️ فصل 28 — Minimal APIs

🔹 86. ءMinimal API چه تفاوتی با یک API سنتی مبتنی بر Controller دارد؟
🔐 87. چگونه Endpointهای Minimal API را با استفاده از Authentication و Authorization امن می‌کنید؟
🔄 88. چگونه در Minimal APIs، API Versioning را پیاده‌سازی می‌کنید؟
🧩 89. چه نوع Filterهایی توسط Minimal APIs پشتیبانی می‌شوند؟
🏢 90. استفاده از Minimal APIs در یک Application بزرگ و Enterprise چه مزایا و معایبی دارد؟

🔗 فصل 29 — Middlewareها

⛓️ 91. ترتیب اجرای Middlewareها چگونه است و چرا اهمیت دارد؟
🛠 92. تمام روش‌های ایجاد Middleware در ASP.NET Core را نام ببرید.
⚙️ 93. تفاوت بین Convention-Based Middleware و Factory-Based Middleware چیست؟
🚨 94. چگونه می‌توان Exceptionها را به‌صورت سراسری با استفاده از Middleware مدیریت کرد؟

🎯 فصل 30 — Filterها

🧩 95. انواع اصلی Filterهای موجود در ASP.NET Core را نام ببرید.
🔢 96. چگونه می‌توان ترتیب اجرای Filterها را کنترل کرد؟
⚙️ 97. تفاوت بین Resource Filter و Action Filter چیست؟
🌐 98. منظور از Filter Scopeهای Global، Controller و Action چیست و اولویت اجرای آن‌ها چگونه تعیین می‌شود؟

🌍 فصل 31 — مبانی REST

📈 99. ءRichardson Maturity Model چیست و چه سطوحی دارد؟
🏗 100. شش Architectural Constraint در REST را نام ببرید و هرکدام را به‌طور خلاصه توضیح دهید.
🔄 101. تفاوت بین PUT و PATCH چیست؟
🎯 102. ءIdempotency چیست؟ کدام HTTP Methodها Idempotent هستند؟
🔐 103. تفاوت بین 401 Unauthorized و 403 Forbidden چیست؟
🔗 104. ءHATEOAS چیست و چه ارتباطی با REST Level 3 دارد؟
📄 105. استراتژی‌های رایج برای Pagination در REST APIها چیستند؟
🎛 106. ءData Shaping در REST APIها چیست و چرا مفید است؟

🔄 فصل 32 — API Versioning

🌐 107. تفاوت بین روش‌های Versioning مبتنی بر URL، Query String، Header و Content Negotiation چیست؟
⚠️ 108. چگونه یک API Version را به‌عنوان Deprecated علامت‌گذاری می‌کنید؟
🛡 109. هنگام معرفی یک API Version جدید، چگونه Backward Compatibility را حفظ می‌کنید؟

✅ فصل 33 — Validation

🧪 110. ءFluentValidation چیست و چرا ممکن است آن را به DataAnnotations ترجیح دهید؟
🌳 111. ءFluentValidation چگونه Complex Object Graphها یا Child Collectionها را اعتبارسنجی می‌کند؟
⏳ 112. چگونه در FluentValidation، Asynchronous Validation انجام می‌دهید؟
💉 113. چگونه می‌توان سرویس‌هایی مانند دسترسی به Database را داخل یک FluentValidation Validator تزریق کرد؟
🚨 114. چگونه خطاهای FluentValidation را در APIها به شکل Problem Details برمی‌گردانید؟

🗺 فصل 34 — Mapping

🔄 115. ءAutoMapper چیست و در Solutionهای بزرگ چه مشکلات و اشتباهات رایجی ممکن است ایجاد کند؟
⚡️ 116. ءMapperly چیست و چگونه از Source Generatorها استفاده می‌کند؟
⚖️ 117. استفاده از Mapperly در مقایسه با Mapperهای Runtime مانند AutoMapper چه مزایا و معایبی دارد؟
🧑‍💻 118. چه زمانی Manual Mapping می‌تواند انتخاب بهتری نسبت به استفاده از یک Mapping Library باشد؟
یه اتفاق جالب با AI داره توی تیم‌ های Software می‌افته.
قبلاً می‌گفتیم:
«این Code رو کی نوشته؟»
الان باید یه سوال دیگه هم بپرسیم:
«کی واقعاً می‌فهمتش؟»
#Engineering_Productivity
یه روز تصمیم گرفتم ببینم واقعاً چرا بعضی روزها ۸ ساعت کار می‌کنم، ولی آخر روز حس می‌کنم تقریباً هیچ کاری نکردم.
شروع کردم به نگاه کردن به کارهایی که اون روز انجام داده بودم:
۳۰ دقیقه روی یک Bug.
بعد یک پیام از تیم.
رفتم سراغ یک PR.
بعد یک سؤال از Product.
برگشتم روی Bug.
یک Meeting.
بعد CI شکست.
بعد دوباره PR.
آخر روز شاید ۶-۷ ساعت درگیر کار بودم...
ولی هیچ‌کدوم واقعاً جلو نرفته بود.
مشکل کم‌کاری نبود. Context Switching بود.
هر بار که از یک مسئله خارج می‌شیم و وارد مسئله‌ی دیگری می‌شیم، بخشی از Context قبلی رو از دست می‌دیم.
و وقتی دوباره برمی‌گردیم، باید زمان بذاریم تا یادمون بیاد:
«کجا بودم؟»
«چی داشتم بررسی می‌کردم؟»
«چرا این تصمیم رو گرفتم؟»
برای همین گاهی:
8 Hours Worked
≠
8 Hours of Progress

حتی بدتر...
اگر چند نفر از یک تیم دائماً همدیگه رو Interrupt کنن، مشکل فقط فردی نیست.
کل تیم وارد یک چرخه می‌شه:
Work
↓
Interrupt
↓
Context Switch
↓
Recovery
↓
Work
↓
Interrupt

برای همین بعضی تیم‌ها با اضافه کردن آدم بیشتر، الزاماً سریع‌تر نمی‌شن.
چون ممکنه فقط تعداد Interruptها رو بیشتر کنن.
این روزها سعی می‌کنم هر کاری که نیاز به تمرکز داره رو تا جای ممکن یک‌تکه انجام بدم.Notification کمتر. Meeting کمتر. Taskهای همزمان کمتر.
و مهم‌تر از همه:
کارهای نیمه‌تمام کمتر.
چون Productivity همیشه یعنی سریع‌تر کار کردن نیست.
گاهی یعنی:
اجازه بدی یک Engineer آن‌قدر Context داشته باشه که واقعاً یک مسئله رو تمام کنه.
📌 200+ سوال مصاحبه واقعی (بخش 4️⃣)

[ BUCKET 2 - ASP.NET Core AND EF Core ]

🔧 فصل 35 — Configuration و Options Pattern

📌 119. ءOptions Pattern چیست و چرا استفاده از آن به‌جای دسترسی مستقیم به Configuration ترجیح داده می‌شود؟
🔄 120. تفاوت بین IOptions<T>، IOptionsSnapshot<T و <IOptionsMonitor<T چیست؟
✅ 121. چگونه Configurationای را که به یک Class متصل شده است، با استفاده از DataAnnotations اعتبارسنجی می‌کنید؟
🔐 122. چگونه مقادیر حساس Configuration مانند API Keyها یا Connection Stringها را امن نگه می‌دارید؟

🚨 فصل 36 — Error Handling

🛑 123. ءMiddleware مربوط به UseExceptionHandler چیست و چگونه آن را پیکربندی می‌کنید؟
🆕 124. در NET 8. برای مدیریت سراسری خطاها چه قابلیت جدیدی با نام IExceptionHandler اضافه شده است؟
📋 125. چگونه در ASP.NET Core APIها، Problem Details (RFC 9457) را برمی‌گردانید؟
🔀 126. چگونه Exceptionهای سفارشی را به‌صورت سراسری به HTTP Status Codeهای مشخص نگاشت می‌کنید؟

📝 فصل 37 — Logging

🔍 127. ءStructured Logging چیست و چرا باید از آن استفاده کنیم؟
🏷 128. ءScopeها در ASP.NET Core Logging چیستند و چگونه از آن‌ها استفاده می‌کنید؟
🗄 129. چگونه Logging مربوط به Database Commandهای EF Core را فعال می‌کنید؟
📊 130. چگونه برای محیط Production، Log Aggregation و Monitoring را پیاده‌سازی می‌کنید؟
🔄 131. چگونه در Serilog حجم Logها و Log File Rotation را کنترل می‌کنید؟

📌 فصل 38 — Health Checks

🟢 132. تفاوت بین Liveness Probe و Readiness Probe چیست؟
🩺 133. چگونه یک Custom Health Check پیاده‌سازی می‌کنید؟

💉 فصل 39 — Dependency Injection (DI)

🔄 134. سه DI Lifetime رایج در ASP.NET Core کدام‌اند؟
⚠️ 135. هنگام Inject کردن یک Scoped Service داخل یک Singleton Service چه مشکلاتی ممکن است ایجاد شود؟
🔧 136. چگونه یک Scoped Service را داخل یک Background Task یا Singleton Class Resolve می‌کنید؟
🔄 137. چگونه با Circular Dependencyها در DI برخورد می‌کنید؟
🎯 138. چگونه Conditional Dependency Resolution را پیاده‌سازی می‌کنید؛ مثلاً بر اساس Configuration؟

🗄 فصل 40 — Entity Framework Core

📦 139. تفاوت بین DbContext و DbSet چیست؟
🔗 140. تفاوت بین Eager Loading، Lazy Loading و Explicit Loading چیست؟
⚡️ 141. چه زمانی و چرا باید از ( )AsNoTracking. در Queryها استفاده کنید؟
📊 142. ءQuery Splitting (AsSplitQuery) چیست و چه مشکل Performanceای را حل می‌کند؟
♻️ 143. ءContext Pooling چیست و چگونه به Applicationهای با Throughput بالا کمک می‌کند؟
🔒 144. چگونه Optimistic Concurrency را با استفاده از RowVersion یا Concurrency Token پیاده‌سازی و مدیریت می‌کنید؟
⚡️ 145. چگونه عملیات Batch Update/Delete را در EF Core بدون Load کردن Entityها انجام می‌دهید؟

[ BUCKET 3 — CACHING, SCHEDULING, MESSAGING, AND SECURITY ]

🔐 فصل 41 — Authentication و Authorization

🆔 146. تفاوت بین Authentication و Authorization چیست؟
🍪 147. تفاوت بین Cookie Authentication و JWT Bearer Authentication چیست؟
📌 148. ءRefresh Token چیست؟ جریان Authentication با JWT + Refresh Token را توضیح دهید.
🛡 149. چگونه Policy-Based یا Claim-Based Authorization را پیاده‌سازی می‌کنید؟
📋 150. در مفهوم Authorization Policy، یک Requirement چیست؟
⚙️ 151. چگونه یک Custom Authorization Handler ایجاد می‌کنید؟
🔄 152. یک سناریو را توضیح دهید که در آن Claims Transformation ضروری است و نحوه پیاده‌سازی آن را بیان کنید.

👤 فصل 42 — ASP.NET Core Identity

🔑 153. چگونه قوانین Password مانند طول، پیچیدگی و سایر الزامات را در Identity پیکربندی می‌کنید؟
📱 154. چگونه با استفاده از Identity، Two-Factor Authentication (2FA) را پیاده‌سازی می‌کنید؟
🔒 155. چگونه می‌توان یک User را پس از چند Login ناموفق Lockout کرد؟
🔗 156. چگونه Identity را با Authentication مبتنی بر JWT Token برای APIها یکپارچه می‌کنید؟
🗄 157. چگونه برای یک Database از نوع NoSQL، Custom User Store ایجاد و مدیریت می‌کنید؟

🛡 فصل 43 — ASP.NET Core Security

🌐 158. ءCORS چیست و اجازه دادن به دسترسی گسترده از طریق آن چه خطرات امنیتی دارد؟
✈️ 159. درخواست‌های Preflight OPTIONS در CORS چگونه کار می‌کنند؟
🔐 160. چگونه Signature و Claims یک JWT را اعتبارسنجی می‌کنید؟
⚠️ 161. ءRefresh Tokenها چه خطرات امنیتی‌ای دارند؟
🚫 162. چگونه Refresh Tokenها را مثلاً هنگام Logout یا تغییر Password Invalidate می‌کنید؟
🔑 163. ءOAuth 2.0 چیست و چگونه برای امن‌سازی APIها استفاده می‌شود؟
🆔 164. ءOpenID Connect (OIDC) چیست و چه تفاوتی با OAuth 2.0 دارد؟
📌 ءPagination رو با Offset پیاده کنیم یا Cursor؟

یکی از اون تصمیم‌هاییه که موقع ساختن API خیلی راحت از کنارش رد می‌شیم.
مثلاً می‌خوایم لیست سفارش‌ها رو برگردونیم.
خب معلومه دیگه 😎
می‌زنیم:
SELECT *
FROM Orders
ORDER BY Id
OFFSET 100000
LIMIT 20;

یا توی EF Core:
var orders = await db.Orders
.OrderBy(x => x.Id)
.Skip(100000)
.Take(20)
.ToListAsync();

تموم شد رفت.
هم ساده‌ست، هم خواناست، هم برای صفحه‌بندی خیلی راحت می‌تونیم بگیم:
page=1
page=2
page=3
و الی آخر...
ولی یک لحظه صبر کن... 🤨
واقعاً فکر می‌کنی وقتی رسیدیم به صفحه‌ی مثلاً 10,000، دیتابیس میگه:
«چشم قربان، دقیقاً 20 تا رکوردت رو از این وسط برمی‌دارم»؟ 😎
نه دقیقاً! OFFSET به دیتابیس میگه:
«این 100 هزار رکورد اول رو رد کن، بعد 20 تای بعدی رو بده.»
یعنی رکوردهایی که قرار نیست به Application برسن، باز هم باید در سمت دیتابیس پردازش بشن.
خود PostgreSQL هم صراحتاً میگه رکوردهایی که توسط OFFSET رد می‌شن، همچنان باید توسط Server محاسبه بشن و OFFSETهای بزرگ می‌تونن inefficient باشن.
پس اگر دیتاست کوچیکه؟
احتمالاً اصلاً مسئله‌ی خاصی نداری.
ولی اگر داری با میلیون‌ها رکورد و Pagination عمیق سروکله می‌زنی، داستان فرق می‌کنه.
حالا بریم سراغ گزینه‌ی دوم:
🎯 Cursor / Keyset Pagination

اینجا به‌جای اینکه به دیتابیس بگیم:
«100 هزار تا رکورد رو رد کن»
می‌گیم:
«من تا اینجا اومدم؛ از بعدِ این رکورد ادامه بده.»
مثلاً:
SELECT *
FROM Orders
WHERE Id > 100000
ORDER BY Id
LIMIT 20;

یا در EF Core:
var orders = await db.Orders
.Where(x => x.Id > lastSeenId)
.OrderBy(x => x.Id)
.Take(20)
.ToListAsync();

اینجا دیگه مفهوم اصلی page number نیست.
مفهوم اصلی اینه:
🧠 من آخرین چیزی که دیدم چی بود؟
مثلاً Response اول:
{
"items": [...],
"nextCursor": "100020"
}

Request بعدی:
GET /orders?cursor=100020

و دیتابیس میگه:
WHERE Id > 100020
ORDER BY Id
LIMIT 20

اگر روی Id ایندکس داشته باشیم، دیتابیس می‌تونه خیلی مستقیم‌تر به محدوده‌ی موردنظر برسه.
ءMicrosoft هم در مستندات EF Core، برای Paginationهایی که فقط حرکت صفحه‌به‌صفحه لازم دارند، Keyset Pagination را به‌عنوان جایگزین مناسب Skip/Take معرفی می‌کند.
اما داستان فقط Performance نیست! 👀
فرض کن کاربر صفحه‌ی 2 رو گرفته.
بعد وسط کار، یک Order جدید وارد سیستم می‌شه.
اگر از OFFSET استفاده کنیم، موقع درخواست صفحه‌ی بعدی ممکنه مجموعه‌ی نتایج نسبت به درخواست قبلی جابه‌جا شده باشه و در شرایط تغییر همزمان داده‌ها، بعضی رکوردها دوباره دیده بشن یا بعضی‌ها از دست برن.
ولی Keyset میگه:
«من آخرین رکوردی که دیدم رو می‌دونم؛ از همون نقطه ادامه بده.»
به همین دلیل برای چیزهایی مثل:
📰 Feed
🛒 Order List
💬 Message List
📜 Activity Log
🔔 Notification List
و سیستم‌هایی که کاربر معمولاً فقط می‌خواد:
Next → Next → Next
بره جلو، Cursor Pagination خیلی جذاب می‌شه.
اماااااا... 😏
اینجا هم قرار نیست بگیم: Cursor > Offset
و تمام!
چون Cursor یک محدودیت مهم داره.
فرض کن کاربر می‌گه:
«برو صفحه 873!»

با Cursor این کار به اون سادگی Offset نیست.
چون Cursor اساساً برای حرکت ترتیبی روی یک Result Set طراحی شده، نه Jump کردن مستقیم به یک Page Number.
پس مثلاً برای یک: 👨‍💼 Admin Panel
که کاربر می‌خواد بگه:
Page 1 | 2 | 3 | ... | 50
و مستقیماً بره صفحه 30...
Offset Pagination
هنوز می‌تونه انتخاب کاملاً معقولی باشه.
اما برای یک: 📱 Infinite Scroll
که کاربر فقط میگه:
«بیشتر بیار» Cursor معمولاً انتخاب بهتریه.
پس اگر بخوام خیلی خلاصه تصمیم بگیرم:
🟢 Offset Pagination

وقتی:
▫️تعداد داده خیلی زیاد نیست
▫️کاربر باید مستقیماً به یک Page خاص بره
▫️ءUX بر اساس Page Number طراحی شده
▫️سادگی Implementation برات مهمه

🔵 Cursor / Keyset Pagination

وقتی:
▫️ءDataset بزرگه
▫️ءPagination عمیقه
▫️کاربر معمولاً Next/Previous می‌کنه
▫️ءInfinite Scroll داری
▫️داده‌ها مرتباً Insert/Delete می‌شن
▫️ءPerformance در صفحات عمیق مهمه

و یک نکته‌ی خیلی مهم:
اگر Pagination داری، Order باید deterministic و ترجیحاً unique باشه.
مثلاً فقط:
.OrderByDescending(x => x.CreatedAt)

ممکنه کافی نباشه، چون چند رکورد می‌تونن CreatedAt یکسان داشته باشن.
بهتره مثلاً:
.OrderByDescending(x => x.CreatedAt)
.ThenByDescending(x => x.Id)

داشته باشی تا ترتیب کاملاً مشخص باشه. Microsoft هم روی unique بودن ترتیب برای Pagination تأکید کرده.

🔖هشتگ‌ها:
#Pagination #OffsetPagination #CursorPagination #KeysetPagination
سلام عزیزان
کمی بخاطر سر شلوغی این موضوع عقب افتاد اما می تونید جزئیات میت ها رو در سایت زیر ببینید.

https://thisisnabi.dev

برای پرداخت 10x developer همون طور که قول دادم 50% تخفیف بگیرید.
اما چون system design هنوز پیش ثبت نامه شرایط پرداخت 4 قسطه اسنپ پی رو اگر دارین می تونید باهاش پرداخت انجام بدید.
برای جزئیات پرداخت به @thisisnabi_admin پیام بدید.
📌 200+ سوال مصاحبه واقعی (بخش 5️⃣)

[ BUCKET 3 — CACHING, SCHEDULING, MESSAGING, AND SECURITY ]

⚡️ فصل 44 — Caching

💾 165. ءIDistributedCache چیست و چه تفاوتی با Memory Cache دارد؟
⏳ 166. چگونه Cache Expiration و Eviction Policyها را برای Redis پیاده‌سازی می‌کنید؟
🚀 167.ءOutputCache در ASP.NET Core چیست و چگونه آن را برای Endpointها پیکربندی می‌کنید؟
🎯 168. چگونه می‌توان OutputCache را بر اساس پارامترهای Request یا هویت کاربر (User Identity) تغییر داد؟
🔀 169. ءHybridCache چیست و چگونه In-Memory Cache و Distributed Cache را با یکدیگر ترکیب می‌کند؟
🏗 170. چگونه از HybridCache با یک استراتژی L1/L2 در ASP.NET Core استفاده می‌کنید؟
🔥 171. ءFusionCache چیست و چه مشکلی را که HybridCache دارد، حل می‌کند؟

⏰ فصل 49 — Task Scheduling

⚙️ 172. ءBackground Service در ASP.NET Core چیست؟ چه زمانی Start و چه زمانی Stop می‌شود؟
🔄 173. تفاوت بین Background Service و IHostedService چیست؟
🚨 174. اگر یک Unhandled Exception در یک Background Service رخ دهد، چه اتفاقی می‌افتد؟
📅 175. چگونه با استفاده از Quartz.NET و Cron Expressionها، Taskهای تکرارشونده (Recurring Tasks) را زمان‌بندی می‌کنید؟
💉 176. چگونه Dependencyها را داخل Quartz Jobها Inject می‌کنید؟
🛑 177. برای جلوگیری از اجرای چندباره یک Scheduled Job در چند Instance مختلف، از چه استراتژی‌هایی می‌توانید استفاده کنید؟
🌐 178. چگونه یک راهکار Distributed Scheduling را برای اجرای Taskها روی چند Server معماری می‌کنید؟

📨 فصل 50 — Event Messaging

🔄 179. ءMediatR چیست و کدام Design Pattern را پیاده‌سازی می‌کند؟
📢 180. چگونه با استفاده از MediatR، Notification ارسال و Handle می‌کنید؟
⚠️ 181. استفاده بیش از حد (Overuse) از MediatR در یک پروژه چه معایب و مشکلاتی می‌تواند ایجاد کند؟
📨 182. ءMassTransit چیست و چه نقشی در Event-Driven Architecture دارد؟
🐇 183. چگونه MassTransit را با RabbitMQ یا یک Message Broker دیگر پیکربندی می‌کنید؟
📦 184. چگونه Messageها (Event / Command) را با استفاده از MassTransit تعریف و Consume می‌کنید؟
🚀 185. قابلیت‌های پیشرفته MassTransit مانند Sagaها یا Message Retry Policyها چیستند؟

[ BUCKET 4 — SYSTEM DESIGN AND ARCHITECTURE ]

🌐 فصل 51 — APIs، SDKs و Resilience

⚠️ 186. اگر برای هر API Call یک HttpClient جدید ایجاد کنید، چه اتفاقی می‌افتد؟
🏭 187. ءHttpClientFactory چیست و چرا در ASP.NET Core معرفی شد؟
🎯 188. ءTyped Client چیست و چه تفاوتی با Named Client دارد؟
🔗 189. ءRefit چیست و چگونه با استفاده از آن یک API Interface تعریف می‌کنید؟
⛓️ 190. ءDelegatingHandler چیست و کاربردهای رایج آن چیستند؟
🛡 191. ءPolly چیست و چگونه در کنار HttpClientFactory استفاده می‌شود؟
🔄 192. یک مثال از اضافه کردن Retry Policy با استفاده از Polly به HttpClientFactory ارائه دهید.
🚨 193. چگونه استراتژی Circuit Breaker را با استفاده از Polly پیاده‌سازی می‌کنید؟
🛟 194. چگونه استراتژی Fallback را با استفاده از Polly پیاده‌سازی می‌کنید؟
🔐 195. چگونه Authentication و Token Refresh را برای Outgoing HTTP Requests مدیریت می‌کنید؟

🔭 فصل 52 — OpenTelemetry & Observability

📊 196. سه ستون اصلی (Three Pillars) مربوط به Observability در OpenTelemetry چیستند؟
🏗 197. ءOpenTelemetry Collector چیست و چه نقشی دارد؟
🔍 198. در مفهوم Distributed Tracing، یک Span و یک Trace چیستند؟
🔗 199. چگونه Distributed Context Propagation و Baggage را بین Microserviceها مدیریت می‌کنید؟
🎯 200. ءSampling در OpenTelemetry چیست و چرا از Strategyهای مختلف Sampling استفاده می‌کنید؟
📈 201. چگونه در دات نت، Custom Metrics مانند Counterها و Histogramها ایجاد می‌کنید؟
🔗 202. ءTrace Links در OpenTelemetry چیستند و چه زمانی مفید هستند؟
📊 203.چگونه OpenTelemetry را برای مدیریت داده‌های High-Cardinality در Metricها پیکربندی می‌کنید؟

🐳 فصل 54 — Build & Deploy / Distributed Systems

📦 204.تفاوت بین Framework-Dependent Deployment و Self-Contained Deployment چیست؟
🎯 205.چگونه یک برنامه NET. را برای یک Runtime مشخص، مانند win-x64 یا linux-x64، Publish می‌کنید؟
⚙️ 206.چگونه فایل‌های appsettings مخصوص هر Environment را در Publish Output پیکربندی می‌کنید؟
🐳 207.چگونه برای یک برنامه ASP.NET Core یک Dockerfile می‌نویسید؟
🏗 208.ءMulti-Stage Docker Build چیست و چرا باید از آن استفاده کنید؟
📦 209.چگونه حجم (Size) یک Docker Image را برای برنامه‌های NET. بهینه می‌کنید؟
🔐 210.چگونه Environment Variableها و Configuration را به یک Docker Container منتقل می‌کنید؟
📌 برای Order فقط یک Status داشته باشیم یا بریم سراغ State Machine؟ 🤔
فرض کن داریم یک فروشگاه اینترنتی میسازیم و Orderمون چندتا وضعیت داره:
Pending
Paid
Processing
Shipped
Delivered
Cancelled

خب معلومه دیگه 😎
یک enum می‌سازیم:
public enum OrderStatus
{
Pending,
Paid,
Processing,
Shipped,
Delivered,
Cancelled
}

بعد داخل Order:
public OrderStatus Status { get; private set; }

هرجا هم خواستیم وضعیت رو عوض کنیم:
order.Status = OrderStatus.Paid;

تموم شد رفت. 😎
هم ساده‌ست، هم خواناست، هم Database هم فقط یک ستون Status داره.
ولی یه لحظه صبر کن...
واقعاً هر Statusای می‌تونه به هر Status دیگه‌ای تبدیل بشه؟ 🤨
مثلاً:
Pending → Paid
Paid → Processing
Processing → Shipped
Shipped → Delivered

این‌ها منطقی به نظر میرسن.
ولی این چی؟
Delivered → Pending

یا:
Shipped → Paid

یا حتی:
Cancelled → Shipped

احتمالاً نه!
پس مشکل از خود Status نیست.
مشکل اینجاست که اگر فقط یک enum داشته باشیم، این enum به‌تنهایی هیچ چیزی درباره‌ی قوانین انتقال بین وضعیت‌ها نمیگه.
یعنی این کد:
order.Status = OrderStatus.Delivered;

از نظر #C کاملاً معتبره.
ولی از نظر Business ممکنه کاملاً غیرمعتبر باشه.
اینجاست که معمولاً یکی میگه:
«پس قبلش if می‌ذاریم.»

مثلاً:
if (order.Status != OrderStatus.Shipped)
throw new InvalidOperationException();

order.Status = OrderStatus.Delivered;

خب...
بعد یک ماه میشه:
if (status == OrderStatus.Pending)
{
...
}
else if (status == OrderStatus.Paid)
{
...
}
else if (status == OrderStatus.Processing)
{
...
}

بعد یک Requirement جدید میاد:
اگر Payment Failed شد، Order دوباره Payment بشه.

بعد یکی دیگه:
اگر Customer درخواست Cancellation داد، فقط قبل از Shipment اجازه بده.

بعد:
ءAdmin بتونه یک Order رو از حالت Processing به Cancelled ببره، ولی Customer نتونه.
و ناگهان...💀 Business Ruleهای مربوط به Lifecycle سفارش پخش شدن توی:
Controller
Service
Handler
Domain
Background Job
...

و هرکس هم یک قانون متفاوت نوشته.
اینجا دقیقاً جاییه که State Machine می‌تونه ارزش پیدا کنه.
ءState Machine اساساً میگه:
«ءOrder فقط یک Status نداره؛ یک Lifecycle داره و فقط بعضی Transitionها مجاز هستند.»
حالا به‌جای اینکه هرجای سیستم بنویسیم:
order.Status = OrderStatus.Shipped;

میگیم:
order.Ship();

و خود Domain تصمیم می‌گیره آیا این Transition مجازه یا نه.
مثلاً:
public void Ship()
{
if (Status != OrderStatus.Processing)
throw new InvalidOperationException(
"Only processing orders can be shipped.");

Status = OrderStatus.Shipped;
}

حالا اگر کسی بگه:
order.Ship();

و Order هنوز Pending باشه، خود Domain جلوش رو می‌گیره.
این خیلی بهتر از اینه که امیدوار باشیم همه‌ی Callerها قبلش if درست نوشته باشن. 😏
اماااا...
اینجا هم نباید سریع نتیجه بگیریم:
«پس State Machine همیشه بهتره!»

نه.
اگر سیستم ما یک Order خیلی ساده داره:
Pending → Completed

واقعاً لازم نیست برای دو وضعیت یک State Machine عظیم درست کنیم.
پس State Machine قرار نیست صرفاً چون اسمش خفن‌تره وارد پروژه بشه.
مسئله اینه که:
آیا Lifecycle موجودیت، خودش دارای پیچیدگی Business است؟
اینجا State Machine می‌تونه خیلی خواناتر و قابل‌کنترل‌تر باشه.
حتی می‌تونی Ruleهایی مثل این داشته باشی:
Customer فقط تا قبل از

Shipped
می‌تونه Cancellation درخواست کنه.

یا:

ءRefund فقط وقتی مجازه که Payment موفق بوده باشه.

یا:
ءShipment فقط بعد از Payment موفق ایجاد بشه.

این‌ها دیگه صرفاً Status نیستن.
این‌ها Business Rules مربوط به Transitionها هستن.
بعضیا فکر می‌کنن وقتی State Machine داریم، دیگه Status لازم نیست.
نه! State Machine و Status لزوماً رقیب هم نیستن.
🗄 10 قابلیت کمترشناخته‌شده SQL که هر Developer باید بداند(پارت 1️⃣)
بیشتر Developerها احتمالاً فقط از حدود 20 درصد قابلیت‌های SQL استفاده می‌کنند.
آن‌ها SELECT، JOIN و GROUP BY می‌نویسند و همان‌جا متوقف می‌شوند.
اما SQL یک لایه‌ی دوم هم دارد؛ قابلیت‌هایی که می‌توانند یک صفحه کد Application یا سه Query جداگانه را به یک Statement تمیز و یکپارچه تبدیل کنند.⚡

ءDeveloperهای Senior همیشه به سراغ این قابلیت‌ها می‌روند. بسیاری از Developerهای Junior و Mid-level حتی یک‌بار هم آن‌ها را ندیده‌اند.
هیچ‌کدام از این قابلیت‌ها جدید یا عجیب‌وغریب نیستند. آن‌ها همین حالا در Databaseای که استفاده می‌کنید وجود دارند و منتظرند تا از آن‌ها استفاده کنید.
🚀امروز می‌خواهم ۱۰ قابلیت کمترشناخته‌شده‌ی SQL را به شما نشان بدهم که هر Developer باید آن‌ها را بشناسد.

📌در این مطلب، موارد زیر را بررسی می‌کنیم:

🔸️Common Table Expressions یا CTE
🔹️Window Functions
🔸️LATERAL Joins
🔹️GROUPING SETS، ROLLUP و CUBE
🔸️عبارت FILTER در Aggregateها
🔹️UPSERT با استفاده از INSERT ... ON 🔸️CONFLICT
🔹️پشتیبانی از JSON
🔸️Computed / Generated Columns
🔹️TABLESAMPLE
🔸️Partial Indexes

بریم سراغشون. 🚀

تمام Queryهای این مطلب روی Database
ءPostgreSQL تست شده‌اند. بیشتر این قابلیت‌ها در Databaseهای دیگر نیز وجود دارند، اما Syntax دقیق آن‌ها ممکن است متفاوت باشد؛ در طول مطلب به تفاوت‌های اصلی اشاره می‌کنم.

1️⃣ ءCommon Table Expressions یا CTE

یک Query پیچیده که همه‌چیز در یک Statement داخل آن فشرده شده باشد، خواندنش سخت و تغییر دادنش حتی سخت‌تر است.
ءCommon Table Expression یا همان CTE به شما اجازه می‌دهد با استفاده از Keyword مربوط به WITH، آن Query را به چند مرحله‌ی نام‌گذاری‌شده و پشت‌سرهم تقسیم کنید.🧩
هر مرحله مانند یک Result موقت و نام‌گذاری‌شده است که می‌توانید در مراحل بعدی روی آن کار کنید.
WITH recent_shipments AS (
SELECT
s.id,
s.number,
s.carrier,
s.status,
s.created_at
FROM shipments.shipments s
WHERE s.created_at >= CURRENT_DATE - INTERVAL '30 days'
),
shipment_details AS (
SELECT
rs.number,
rs.carrier,
rs.status,
COUNT(si.id) AS total_items,
SUM(si.quantity) AS total_quantity
FROM recent_shipments rs
LEFT JOIN shipments.shipment_items si
ON rs.id = si.shipment_id
GROUP BY
rs.number,
rs.carrier,
rs.status
)
SELECT
number AS shipment_number,
carrier,
status,
total_items,
total_quantity
FROM shipment_details
ORDER BY total_quantity DESC;

این Query شامل دو بخش نام‌گذاری‌شده است. recent_shipments، Shipmentهایی را که در 30 روز گذشته ایجاد شده‌اند انتخاب می‌کند.📅
سپس shipment_details روی آن Result کار می‌کند، Itemهای مربوط به Shipment را Join می‌کند و تعداد و مقدار آن‌ها را Aggregate می‌کند.
در نهایت، SELECT اصلی از shipment_details می‌خواند؛ درست مثل اینکه با یک Table معمولی کار می‌کند.
مزیت اصلی این است که Query را می‌توانید از بالا به پایین بخوانید؛ درست مثل مراحل یک دستورالعمل، به‌جای اینکه مجبور باشید Nested Subqueryها را از داخل به بیرون دنبال کنید.🧠
ءCTEها از Recursive Query نیز پشتیبانی می‌کنند؛ با استفاده از WITH RECURSIVE.
این قابلیت زمانی بسیار کاربردی است که با داده‌های Hierarchical مانند:
🏢ساختار سازمانی
🌳درخت دسته‌بندی‌ها
📂ءCategory Tree
🔗ساختار Parent/Child
کار می‌کنید.
یک Common Table Expression می‌تواند داخل Statementهای SELECT، INSERT، UPDATE یا DELETE استفاده شود.
Forwarded from Mahi in Tech
توی سیستم‌های High-Load، یکی از چالش‌های همیشگی اینه که خیلی سریع بفهمیم یک دیتای خاص وجود داره یا نه. اگر بخوایم برای هر چک کردن ساده به‌طور مستقیم سراغ دیتابیس بریم یا حتی به صورت کامل روی Cache حساب کنیم، هم منابع زیادی درگیر می‌شه و هم Latency بالا میره.

یکی از رویکردهای بهینه و جذاب برای حل این مسئله، استفاده از Bloom Filter هست.
بلوم فیلتر یک Data Structure احتمالاتی 🥴 هست که با کمترین میزان مصرف مموری و سرعت خیره‌کننده، بهمون میگه یک آیتم وجود داره یا نه.

سناریوی واقعی: انتخاب یوزرنیم در تلگرام
تلگرام صدها میلیون کاربر داره. وقتی شما موقع ثبت‌نام داری یوزرنیم تایپ می‌کنی، به ازای هر کاراکتری که می‌زنی باید چک بشه که این یوزرنیم آزاد هست یا نه. اگر تلگرام بخواد برای هر تایپ شما یک کوئری به دیتابیس اصلیش بزنه، دیتابیس در عرض چند ثانیه از حجم درخواست‌ها نابود می‌شه!
راه‌حل چیه؟ تلگرام تمام یوزرنیم‌های ثبت‌شده رو میده به یک Bloom Filter که توی رم قرار داره. وقتی شما یوزرنیم جدید رو تایپ می‌کنی، بلوم فیلتر در کسری از میلی‌ثانیه چک می‌کنه. اگر بگه «این یوزرنیم به‌طور قطع وجود نداره»، تلگرام همون لحظه تیک سبز رو بهت نشون میده و دیگه کاری به دیتابیس نداره (صرفه‌جویی عظیم در منابع). اما اگر بلوم فیلتر بگه «ممکن هست وجود داشته باشه»، تلگرام تازه اونجا میره از دیتابیس می‌پرسه که "مطمئنی این یوزرنیم پر شده؟" تا وضعیت دقیق رو بهت بگه. (که البته تلگرام چنین کاری نمی‌کنه و مثال بود=))

حالا این بلوم فیلتر چطور کار می‌کنه؟
پشت صحنه، Bloom Filter در واقع فقط یک آرایه طولانی از Bitهاست که اول کار همه‌شون صفر هستن. در کنارش، چند تا تابع Hash مستقل و سریع هم داریم.
وقتی می‌خوایم یک دیتای جدید رو به سیستم اضافه کنیم، این دیتا رو به توابع Hash می‌دیم. خروجی این توابع، ایندکس‌هایی از همون آرایه بیت‌هاست. بعد میریم اون ایندکس‌ها رو برابر با ۱ قرار می‌دیم.

موقع جستجو دوباره همون دیتای ورودی رو هش می‌کنیم و ایندکس‌ها رو چک می‌کنیم:
۱. اگر حتی یکی از اون بیت‌ها صفر باشه، سیستم با قطعیت ۱۰۰٪ میگه این دیتا «به‌طور قطع وجود نداره».
۲. اگر همه بیت‌ها ۱ باشن، سیستم میگه این دیتا «احتمالا وجود داره».

چرا می‌گیم احتمالا؟ چون ممکنه اون بیت‌ها قبلا به‌خاطر هش شدنِ دیتای دیگه‌ای ۱ شده باشن (همون پدیده Hash Collision). یعنی ما توی Bloom Filter خطای False Positive داریم، اما False Negative اصلا نداریم.

البته که این ساختار محدودیت‌های خودش رو هم داره؛ به‌طور مثال حذف کردن یک آیتم از بلوم فیلتر در پیاده‌سازی‌های استانداردش یه‌جورایی غیرممکنه؛ چون با صفر کردن یک بیت، ممکن هست دیتای دیگه‌ای که از همون بیت استفاده می‌کرده رو هم خراب کنیم.

در نهایت، در ازای پذیرش اون احتمال کمِ False Positive، سیستمی به دست میاد که می‌تونه وجود میلیون‌ها رکورد رو فقط با چند مگابایت RAM در لایه Application چک کنه و زیرساخت دیتابیس شما رو از شر درخواست‌های بیهوده نجات بده.
📦 چطور یک فایل چندگیگابایتی را Upload می‌کنیم و اگر اینترنت قطع شد، از همان‌جا ادامه می‌دهیم؟
فرض کن داری یک فایل 8GB آپلود می‌کنی.
۵۰٪ آپلود شده...
Upload: 4.0 GB / 8.0 GB

بعد اینترنت قطع می‌شود. 😐
اگر Upload به‌صورت یک درخواست ساده انجام شده باشد، ممکن است مجبور شوی دوباره از ابتدا شروع کنی.
یعنی:
8 GB
↓
Connection Lost
↓
💀
↓
Upload again

اما سیستم‌های Resumable Upload دقیقاً برای حل همین مشکل طراحی شده‌اند.
ایده‌ی اصلی خیلی ساده است:
فایل بزرگ را طوری منتقل کن که بتوانیم بدانیم تا کجای آن با موفقیت دریافت شده و بعداً از همان نقطه ادامه دهیم.


📌 به‌جای یک Upload بزرگ، انتقال را قابل ادامه می‌کنیم
در یک Upload معمولی ممکن است کل فایل در یک درخواست ارسال شود:
Client
|
|------ 8GB ------> Server

اگر ارتباط وسط انتقال قطع شود، درخواست شکست می‌خورد و ادامه‌دادن آن دشوار است.
یک مثال واقعی از پروتکل tus
فرض کنیم Upload تا byte شماره‌ی 70 پیش رفته است. Client وضعیت Upload را می‌پرسد:
HEAD /files/abc123

Server پاسخ می‌دهد:
Upload-Offset: 70

یعنی:
تا byte 70

قبلاً دریافت شده.
حالا Client ادامه‌ی فایل را می‌فرستد:
PATCH /files/abc123

Upload-Offset: 70
Content-Type: application/offset+octet-stream

[remaining bytes]

ءServer بعد از دریافت آن بخش پاسخ می‌دهد:
Upload-Offset: 100

یعنی Upload حالا تا byte شماره‌ی 100 پیش رفته است.
این رفتار در specification رسمی tus تعریف شده و صرفاً یک الگوی پیشنهادی نیست.
در Resumable Upload، انتقال در چند درخواست انجام می‌شود:
Client
|
|--- Part 1 ---> Server
|
|--- Part 2 ---> Server
|
|--- Part 3 ---> Server
|
|--- Part 4 ---> Server
|
...

در مستندات رسمی Google Cloud نیز Resumable Upload دقیقاً به‌عنوان روشی برای ادامه‌دادن انتقال بعد از اختلال ارتباط معرفی شده است؛ هر درخواست می‌تواند بخشی از Object را منتقل کند.

📌ءPause هم در اصل یعنی چه؟
یک نکته‌ی مهم:
ءPause و Resume الزاماً دو قابلیت کاملاً جدا نیستند.
وقتی Upload قابل Resume باشد، Client می‌تواند ارسال داده را متوقف کند و بعداً با همان Upload Session ادامه دهد؛ به شرط اینکه Session هنوز معتبر باشد.
مثلاً:
10:00
Upload → 2GB

10:05
Pause ⏸️

10:30
Resume ▶️

Continue → 2GB

در Google Cloud Storage، یک Resumable Upload با یک session URI انجام می‌شود و همان session برای ادامه‌ی انتقال استفاده می‌شود. مستندات Google می‌گوید این session می‌تواند تا یک هفته فعال بماند.
پس یک نکته‌ی مهم داریم:
ءResume به وجود یک state/session قابل ادامه وابسته است.

آیا همیشه باید فایل را به Chunkهای کوچک تقسیم کنیم؟
نه.
این یکی از جاهایی است که خیلی از توضیحات ساده، بیش از حد کلی‌گویی می‌کنند. Resumable Upload الزاماً به معنی این نیست که:
8GB
↓
8000 × 1MB

حتماً باید چنین کاری انجام دهیم.
ءGoogle Cloud صراحتاً اشاره می‌کند که در بعضی شرایط بهتر است انتقال در یک chunk بزرگ انجام شود و chunkهای کوچک‌تر هزینه و latency بیشتری ایجاد می‌کنند. Chunking زمانی می‌تواند مفید باشد که مثلاً محدودیت اندازه‌ی درخواست وجود داشته باشد یا بخواهیم مقدار داده‌ای که در صورت شکست دوباره باید ارسال شود را محدود کنیم.
پس:
ءChunking یک تکنیک برای پیاده‌سازی و کنترل Upload است؛ Resumability مفهوم بزرگ‌تری است.

ءIntegrity Check هم مهم است
فرض کن فایل 8GB است.
ما فقط نمی‌خواهیم بگوییم:
8GB received ✅

باید مطمئن شویم:
چیزی که دریافت کرده‌ایم همان چیزی است که Client قصد ارسالش را داشته.

برای همین، سیستم‌های Storage می‌توانند از checksum / hash برای بررسی integrity استفاده کنند.
ءGoogle Cloud در مستندات Resumable Upload توصیه می‌کند برای Object نهایی integrity check انجام شود؛ از جمله استفاده از Content-MD5 برای بررسی اینکه Object نهایی با فایل اصلی مطابقت دارد.
این موضوع مخصوصاً برای فایل‌های بزرگ اهمیت بیشتری پیدا می‌کند، چون انتقال آن‌ها زمان بیشتری طول می‌کشد.
#کلیشه
«این متن با AI نوشته شده، پس ارزش خوندن نداره.»
از کجا معلوم؟
شاید نویسنده، حرف خوبی برای گفتن داشته باشه، ولی نویسنده‌ خوبی نباشه.
شاید فقط ایده رو داده باشه به AI
تا بهتر بیانش کنه.
ناتوانی در نوشتن، لزوما ناتوانی در فکر کردن نیست.
اگه همون حرف رو یک نویسنده‌ی ضعیف ولی یک مهندس قوی گفته باشه و AI فقط بهتر بیانش کرده باشه، چی؟
حالا سوال:
ما کیفیت فکر رو قضاوت می‌کنیم
یا کیفیت جمله‌بندی رو؟
🗄 10 قابلیت کمترشناخته‌شده SQL که هر Developer باید بداند(پارت 2️⃣)


🪟 2. Window Functions

گاهی به محاسبه‌ای روی Rowهای مرتبط نیاز دارید، اما همچنان می‌خواهید هر Row به‌صورت جداگانه در نتیجه باقی بماند.
یک GROUP BY، Rowها را به یک Row برای هر Group تبدیل می‌کند. اما یک Window Function روی مجموعه‌ای از Rowها — یعنی همان Window — محاسبه انجام می‌دهد، در حالی که هر Row را دست‌نخورده حفظ می‌کند.
SELECT
number,
carrier,
created_at,
ROW_NUMBER() OVER (
PARTITION BY carrier
ORDER BY created_at DESC
) AS shipment_sequence,
RANK() OVER (
PARTITION BY carrier
ORDER BY created_at DESC
) AS shipment_rank
FROM shipments.shipments;

SELECT
number,
status,
created_at,
LAG(status) OVER (
ORDER BY created_at
) AS previous_status,
LEAD(carrier) OVER (
ORDER BY created_at
) AS next_carrier
FROM shipments.shipments;

ءQuery اول، Shipmentهای هر Carrier را بر اساس تاریخ رتبه‌بندی می‌کند. ROW_NUMBER() یک Sequence یکتا درون هر Carrier ایجاد می‌کند؛ یعنی همان PARTITION BY carrier. اما RANK() نیز همین کار را انجام می‌دهد، با این تفاوت که Rowهای دارای مقدار برابر، رتبه‌ی مشترک دریافت می‌کنند.
ءQuery دوم از LAG و LEAD استفاده می‌کند تا بدون نیاز به Self-Join، به Row قبلی و بعدی در ترتیب مشخص‌شده نگاه کند؛ در اینجا، Status قبلی و Carrier بعدی.
ءWindow Functionها روشی هستند که با استفاده از آن‌ها می‌توانید Running Total، Ranking، Moving Average و مقایسه‌ی Rowبه‌Row بسازید.
آن‌ها بخشی از Standard SQL هستند و در PostgreSQL، SQL Server، Oracle و MySQL 8+ کار می‌کنند.

🔗 3. LATERAL Joins

یک JOIN معمولی، دو Table را بر اساس یک شرط با یکدیگر Match می‌کند. اما نمی‌تواند برای هر Row از Table اول، یک Query جداگانه اجرا کند.
یک LATERAL JOIN می‌تواند این کار را انجام دهد. این قابلیت به یک Subquery در سمت راست اجازه می‌دهد به Columnهای Table سمت چپ Reference داشته باشد و برای هر Row، یک‌بار اجرا شود؛ بنابراین برای مسئله‌های Top-N-Per-Group بسیار مناسب است.
-- For each carrier, grab their single most recent shipment

SELECT
c.carrier,
s.number,
s.status,
s.created_at
FROM (
SELECT DISTINCT carrier
FROM shipments.shipments
) c
CROSS JOIN LATERAL (
SELECT
number,
status,
created_at
FROM shipments.shipments
WHERE carrier = c.carrier
ORDER BY created_at DESC
LIMIT 1
) s;

برای هر Carrier منحصربه‌فرد، Subquery مربوط به LATERAL جدیدترین Shipment آن Carrier را انتخاب می‌کند؛ یعنی ORDER BY created_at DESC LIMIT 1.
نکته‌ی اصلی این بخش عبارت WHERE carrier = c.carrier است. Query داخلی می‌تواند carrier مربوط به Row بیرونی را ببیند؛ قابلیتی که یک Subquery معمولی نمی‌تواند انجام دهد.
این روش، تمیزترین راه برای بیان مسئله‌ی «جدیدترین Row برای هر Group» یا «۳ مورد برتر برای هر Category» است، بدون اینکه به Window Function نیاز داشته باشید.
نکته: در SQL Server، همین قابلیت با CROSS APPLY نوشته می‌شود و برای نسخه‌ی مشابه Left Join از OUTER APPLY استفاده می‌شود. PostgreSQL از CROSS JOIN LATERAL و LEFT JOIN LATERAL استفاده می‌کند.
یه چیز عجیب درباره‌ی Productivity:
گاهی اضافه کردن آدم به یک پروژه،
پروژه رو سریع‌تر نمیکنه.
ممکنه حتی کندترش کنه.
چون آدم جدید یعنی:
ءContext جدید
جلسه‌ی بیشتر
ءCommunication بیشتر
ءCoordination بیشتر
و گاهی Dependency بیشتر.
برای همین:
10 نفر روی یک مسئله، لزوماً از 3 نفر سریع‌تر نیستن.
گاهی مشکل کمبود آدم نیست.
مشکل اینه که:
مسئله بیش از حد آدم میخواد تا حل بشه.
و اونجا شاید باید Architecture یا Process رو ساده‌تر کرد، نه Team رو بزرگ‌تر.
🚕 اوبر چگونه نزدیک‌ترین راننده را در مقیاس بزرگ پیدا می‌کند؟

فرض کن در یک شهر بزرگ، هزاران یا حتی میلیون‌ها راننده و مسافر به‌صورت هم‌زمان در حال حرکت هستند.
یک مسافر درخواست سفر می‌دهد و سیستم باید خیلی سریع جواب بدهد:
کدام راننده را به این مسافر اختصاص بدهیم؟

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

اما این راه‌حل در مقیاس بزرگ، خیلی زود تبدیل به یک مشکل جدی می‌شود. 😅
❌ چرا بررسی همه‌ی راننده‌ها جواب خوبی نیست؟
فرض کن در یک شهر، ۱۰۰ هزار راننده‌ی آنلاین داریم.
اگر برای هر درخواست سفر، فاصله‌ی تمام ۱۰۰ هزار راننده را بررسی کنیم، هزینه‌ی هر درخواست تقریباً به تعداد کل راننده‌ها وابسته می‌شود.
حالا اگر هزاران درخواست در هر لحظه وارد شوند، سیستم باید دائماً:
موقعیت راننده‌ها را دریافت کند؛ فاصله‌ی آن‌ها را محاسبه کند؛ راننده‌های نامناسب را حذف کند؛
و در نهایت بهترین گزینه را انتخاب کند.
مشکل فقط تعداد راننده‌ها نیست.
موقعیت راننده‌ها هم ثابت نیست. هر چند لحظه ممکن است راننده:
چند خیابان جلوتر رفته باشد؛ سفر جدیدی قبول کرده باشد؛ آفلاین شده باشد؛
یا دیگر برای دریافت سفر در دسترس نباشد.
پس ما با یک Query ساده‌ی Database طرف نیستیم.
ما با ترکیبی از این مسائل روبه‌رو هستیم:
Geospatial Search
+
Real-Time Location Updates
+
Distributed Systems
+
Matching Optimization

🗺 ایده‌ی اول: نقشه را به Cell تقسیم کنیم
به‌جای اینکه تمام راننده‌ها را در یک لیست بزرگ نگه داریم، نقشه را به بخش‌های کوچک‌تر تقسیم می‌کنیم.
برای مثال:
+---------+---------+---------+
| Cell A | Cell B | Cell C |
+---------+---------+---------+

حالا هر راننده در یک Cell قرار می‌گیرد.
وقتی مسافر در Cell E درخواست سفر می‌دهد، لازم نیست تمام راننده‌های شهر بررسی شوند.
ابتدا این بخش‌ها را بررسی می‌کنیم:
Cell E
Cell D
Cell F
و Cellهای نزدیک دیگر

این کار تعداد Candidateها را بسیار کمتر می‌کند.
اما یک سؤال مهم وجود دارد:
این Cellها را چطور بسازیم؟
⬡ ءH3؛ سیستم مکانی Uber

ءUber برای کارهای جغرافیایی خودش، سیستم H3 را توسعه داد و Open Source کرد. H3 مخفف این عبارت است:
Hexagonal Hierarchical Geospatial Index

یعنی یک سیستم Index مکانیِ سلسله‌مراتبی که جهان را به Cellهای شش‌ضلعی تقسیم می‌کند.
به‌جای اینکه فقط با Latitude و Longitude کار کنیم، مختصات را به یک شناسه‌ی مکانی تبدیل می‌کنیم.
مثلاً به‌صورت مفهومی:
Latitude: 35.7219
Longitude: 51.3347
↓
H3 Cell ID
↓
8a2a1072b59ffff

شناسه‌ی بالا صرفاً یک نمونه از فرمت H3 است، نه شناسه‌ی واقعی یک راننده.
در کد رسمی H3 نیز می‌توان مختصات جغرافیایی را به یک Cell تبدیل کرد:
latLngToCell(latitude, longitude, resolution)


🔎 هنگام درخواست سفر چه اتفاقی می‌افتد؟

فرض کن مسافر در Cell E قرار دارد.
سیستم می‌تواند ابتدا راننده‌های همین Cell را بررسی کند:
Search(Cell E)

اگر راننده‌ی مناسب پیدا نشد، جست‌وجو را به Cellهای اطراف گسترش می‌دهد:
Search(Cell E)
Search(Neighbors of E)
Search(Neighbors of Neighbors)

به این ترتیب، سیستم به‌جای بررسی تمام شهر، یک ناحیه‌ی محدود را بررسی می‌کند.
اما اینجا یک نکته‌ی مهم وجود دارد:
نزدیک‌ترین راننده الزاماً بهترین راننده نیست
فرض کن دو راننده داریم:
Driver A:
فاصله‌ی مستقیم: 800 متر
اما پشت رودخانه است

Driver B:
فاصله‌ی مستقیم: 1.2 کیلومتر
اما از مسیر مستقیم و خلوت می‌تواند برسد

از نظر فاصله‌ی هندسی: A بهتر است

اما از نظر زمان رسیدن: B ممکن است بهتر باشد
خود Uber نیز توضیح داده که در ابتدا Matching را با این سؤال انجام می‌داد:
چه کسی از همه نزدیک‌تر است؟

اما بعد مشخص شد که «نزدیک‌ترین» همیشه به معنی «سریع‌ترین برای رسیدن» نیست.
عواملی مثل:
ترافیک؛
پل‌ها و بزرگراه‌ها؛
رودخانه‌ها؛
خیابان‌های یک‌طرفه؛
مسیر واقعی رانندگی؛
و زمان رسیدن
می‌توانند نتیجه را تغییر دهند.
بنابراین فرآیند واقعی چیزی شبیه این است:
1. پیدا کردن راننده‌های مکانیِ نزدیک
2. حذف راننده‌های نامعتبر
3. تخمین زمان رسیدن
4. بررسی محدودیت‌ها و شرایط Matching
5. انتخاب Assignment مناسب
🗄 10 قابلیت کمترشناخته‌شده SQL که هر Developer باید بداند(پارت 3️⃣)


📊 4. ءGROUPING SETS، ROLLUP و CUBE

یک Report اغلب به چندین سطح از Summary به‌صورت هم‌زمان نیاز دارد: مجموع‌ها بر اساس Carrier و Status، Subtotalهای هر Carrier و یک Grand Total.
روش ساده این است که چند Query را با استفاده از UNION ALL به یکدیگر متصل کنیم.
ءGROUPING SETS، ROLLUP و CUBE تمام این سطوح را در یک Query تولید می‌کنند.
SELECT
carrier,
status,
COUNT(*) AS shipment_count,
SUM(si.quantity) AS total_quantity
FROM shipments.shipments s
LEFT JOIN shipments.shipment_items si
ON s.id = si.shipment_id
GROUP BY GROUPING SETS (
(carrier, status), -- by carrier & status
(carrier), -- subtotal by carrier
(status), -- subtotal by status
() -- grand total
);

SELECT
carrier,
status,
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS shipment_count
FROM shipments.shipments
GROUP BY ROLLUP (
carrier,
status,
DATE_TRUNC('month', created_at)
);

ءQuery اول دقیقاً Groupingهایی را فهرست می‌کند که به آن‌ها نیاز دارید: بر اساس Carrier و Status، فقط بر اساس Carrier، فقط بر اساس Status و () خالی برای Grand Total.
ءROLLUP در Query دوم، شکل کوتاه‌شده‌ای برای Subtotalهای سلسله‌مراتبی است: Carrier، سپس Carrier و Status، بعد Carrier و Status و Month، و در نهایت Total.
ءCUBE تمام ترکیب‌های ممکن از Columnها را تولید می‌کند.
یک Query جایگزین چهار Query می‌شود و Database سطوح مختلف را در یک Pass محاسبه می‌کند، به‌جای اینکه Table را چندین بار Scan کند.
این قابلیت‌ها بخشی از Standard SQL هستند و در PostgreSQL، SQL Server و Oracle کار می‌کنند.

🧮 5. ءFILTER Clause در Aggregateها

اغلب لازم است فقط Rowهایی را Count یا Sum کنید که یک شرط مشخص را دارند و نتیجه‌ی آن‌ها را به‌صورت جداگانه و کنار هم نمایش دهید.
عبارت FILTER یک شرط را روی یک Aggregate مشخص اعمال می‌کند؛ بنابراین هر Aggregate یک Subset متفاوت را Count می‌کند — همه در یک Row و در یک Pass روی داده‌ها.
SELECT
carrier,
COUNT(*) AS total_shipments,
COUNT(*) FILTER (
WHERE status = 'delivered'
) AS delivered_count,
COUNT(*) FILTER (
WHERE status = 'in_transit'
) AS in_transit_count,
COUNT(*) FILTER (
WHERE status = 'pending'
) AS pending_count,
SUM(si.quantity) FILTER (
WHERE status = 'delivered'
) AS delivered_quantity,
SUM(si.quantity) FILTER (
WHERE status = 'pending'
) AS pending_quantity
FROM shipments.shipments s
LEFT JOIN shipments.shipment_items si
ON s.id = si.shipment_id
GROUP BY carrier;

هر COUNT(*) FILTER (WHERE ...) فقط Rowهای مطابق با شرط را Count می‌کند؛ بنابراین برای هر Carrier، تعداد Shipmentهای Delivered، In-Transit و Pending را در Columnهای جداگانه دریافت می‌کنید.
عبارت زیر:
COUNT(*) FILTER (WHERE status = 'delivered')

خواناتر از روش قدیمی استفاده از CASE است:
SUM(
CASE
WHEN status = 'delivered' THEN 1
ELSE 0
END
)

هدف و منظور عبارت FILTER بسیار واضح‌تر است.

📌 نکته: FILTER توسط PostgreSQL پشتیبانی می‌شود. SQL Server و MySQL این قابلیت را ندارند؛ در آن Databaseها باید از CASE داخل Aggregate استفاده کنید، مانند:

COUNT(
CASE
WHEN status = 'delivered' THEN 1
END
)
یه اشتباه رایج توی طراحی سیستم:
همه‌چیز باید Real-Time باشه.
کاربر چیزی تغییر میده،همه‌جا باید همون لحظه تغییر کنه.
ولی واقعاً لازمه؟
فرض کن کاربر Profile خودش رو تغییر داده.
آیا Notification Service باید همان میلی‌ثانیه تغییر رو ببینه؟
آیا Analytics باید همان لحظه Update بشه؟
آیا Search Index باید دقیقاً همان لحظه تغییر کنه؟
شاید نه.
گاهی: Eventual Consistency نه یک مشکل، بلکه یک تصمیم مهندسیه.
اگر ۲ ثانیه تأخیر قابل‌قبوله، لازم نیست برای ۲ ثانیه تأخیر، کل سیستم رو به هم وابسته کنیم.
هر وابستگی Sync یعنی:
ءLatency بیشتر
ءFailure بیشتری
ءCoordination بیشتر
گاهی سیستم بهتر، سیستمی نیست که همه‌چیز رو سریع‌تر Sync می‌کنه.
سیستمیه که می‌دونه کجا لازم نیست Sync باشه.

🔖هشتگ‌ها:
#SystemDesign #Architecture #SoftwareEngineering #DotNet
📩 سیستم پیامک در مقیاس بزرگ؛ چرا همه‌ی پیامک‌ها نباید در یک صف باشند؟ (Part1️⃣)

فرض کن ساعت ۵ عصر است.
یک سیستم بانکی باید هم‌زمان این پیام‌ها را ارسال کند:
🔐 پیامک OTP برای ورود کاربران
💳 پیامک برداشت و تراکنش مالی
📦 پیامک وضعیت سفارش
📢 پیامک تبلیغاتی
📰 پیامک اطلاع‌رسانی عمومی
حالا تصور کن یک کمپین تبلیغاتی شروع شده و ۷ میلیون پیامک وارد سیستم شده است.
اگر همه‌ی پیامک‌ها را داخل یک صف قرار دهیم، چه اتفاقی می‌افتد؟
[Marketing 1]
[Marketing 2]
...
[Marketing 7,000,000]
[OTP Login]

کاربر منتظر ورود به حسابش است، اما پیامک OTP او پشت میلیون‌ها پیامک تبلیغاتی گیر کرده.
از دید سیستم، صف هنوز در حال کار است.
اما از دید کاربر:
سیستم خراب است! 😐

اینجا باید از Priority Queue استفاده کنیم.

🎯 ایده‌ی اصلی Priority Queue
در صف معمولی، پیام‌ها بر اساس زمان ورود پردازش می‌شوند:
First In → First Out

اما در Priority Queue، هر پیام علاوه بر اطلاعات خودش، یک سطح اهمیت هم دارد:
{
"messageId": "msg-123",
"type": "OTP",
"priority": "Critical"
}

سیستم به‌جای اینکه فقط بپرسد:
کدام پیام زودتر وارد شده؟

می‌پرسد:
کدام پیام مهم‌تر است و باید زودتر پردازش شود؟

برای مثال:
Priority 0 → Critical
Priority 1 → High
Priority 2 → Normal
Priority 3 → Low

نکته‌ی مهم:
ءPriority Queue فقط ترتیب پردازش را مشخص می‌کند؛ ارسال واقعی همچنان به ظرفیت Provider، محدودیت شبکه و وضعیت مقصد وابسته است.


🧩 یک صف یا چند صف؟
برای پیاده‌سازی Priority Queue دو روش اصلی وجود دارد.
❗️روش اول: یک صف با Priority
در این روش همه‌ی پیام‌ها در یک Queue هستند، اما هر پیام Priority دارد:
Queue
├── Message A - Priority 3
├── Message B - Priority 1
├── Message C - Priority 2
└── Message D - Priority 0

ءConsumer باید پیام‌ها را بر اساس Priority دریافت کند.
✅مزیت:
مدیریت ساده‌تر
یک مسیر پردازش
مناسب برای سیستم‌های کوچک‌تر
❌مشکل:
کنترل ظرفیت هر سطح دشوارتر می‌شود.
ممکن است پیام‌های مهم وارد Consumerهای اشغال‌شده توسط پیام‌های کم‌اهمیت شوند.
رفتار واقعی به نوع Broker و تنظیمات Consumer وابسته است.
در RabbitMQ، Priority Queue وجود دارد؛ اما مستندات رسمی آن تأکید می‌کنند که اگر Consumer مقدار زیادی پیام را با prefetch دریافت کرده باشد، پیام Priority بالا ممکن است مجبور شود پشت پیام‌های کم‌اهمیتی که قبلاً به Consumer تحویل داده شده‌اند منتظر بماند.
پس این تنظیم مهم است:
Consumer Prefetch

اگر Prefetch بیش از حد بزرگ باشد، Broker تعداد زیادی پیام را زودتر به Consumer تحویل می‌دهد و فرصت اولویت‌بندی کاهش پیدا می‌کند.
❗️روش دوم: چند صف جداگانه
در سیستم‌های مهم‌تر، معمولاً Priorityها را به Queueهای جدا تقسیم می‌کنیم:
sms-critical
sms-high
sms-normal
sms-low

سپس Dispatcher تصمیم می‌گیرد از کدام صف پیام بردارد.
این مدل کنترل بیشتری می‌دهد و می‌توان برای هر صف Consumer یا ظرفیت جداگانه داشت.

⚠️ مشکل خطرناک: Starvation
فرض کن سیستم همیشه پیامک‌های Critical دریافت می‌کند.
Dispatcher هم همیشه این منطق را اجرا می‌کند:
تا وقتی Critical خالی نشده:
فقط Critical را پردازش کن

در این شرایط، پیامک‌های Low ممکن است هیچ‌وقت پردازش نشوند.
به این مشکل می‌گوییم:
Starvation

یعنی یک گروه از کارها دائماً منابع دریافت نمی‌کنند.
مثلاً:
Critical Queue:
همیشه پر است

Low Queue:
هیچ‌وقت نوبتش نمی‌رسد

این رفتار برای OTP شاید مناسب باشد، اما برای پیامک‌های عادی می‌تواند باعث شود:
پیام‌ها بیش از حد قدیمی شوند؛
کمپین‌ها بی‌ارزش شوند؛
هزینه‌ی نگهداری Queue افزایش پیدا کند؛
و Backlog دائماً رشد کند.

🛠 راه‌حل: Weighted Fair Scheduling
به‌جای اینکه همیشه فقط صف Critical را مصرف کنیم، برای هر صف سهمی تعیین می‌کنیم.
مثلاً:
Critical → 70%
High → 20%
Normal → 8%
Low → 2%

یعنی Dispatcher تلاش می‌کند بیشتر ظرفیت را به پیام‌های مهم بدهد، اما صف‌های دیگر را کاملاً گرسنه نگذارد.
یک مدل ساده:
در هر 100 پیام:

70 پیام از Critical
20 پیام از High
8 پیام از Normal
2 پیام از Low

⏳ ءAging؛ اولویت پیام قدیمی را افزایش بده
یک راه دیگر برای جلوگیری از Starvation، تکنیک Aging است.
ایده:
هرچه یک پیام بیشتر منتظر بماند، اولویت مؤثر آن بیشتر شود.

مثلاً:
Marketing Message:
Priority = Low

بعد از چند دقیقه:
Effective Priority = Normal

و اگر باز هم پردازش نشد:
Effective Priority = High

این فرمول فقط برای توضیح مفهوم است؛ در سیستم واقعی باید مراقب باشیم Aging باعث نشود پیام‌های قدیمی تبلیغاتی ناگهان OTPهای جدید را کنار بزنند.
برای همین معمولاً Priorityهای امنیتی و تراکنشی قوانین سخت‌گیرانه‌تری دارند.