C# Geeks (.NET)
550 subscribers
157 photos
4 videos
177 links
Download Telegram
🧩 ءGitHub Gists؛ یک Git Repository کوچک برای کدهای کوچک!

تا حالا شده یک تکه کد، Regex، Query، کانفیگ یا یک نمونه کوچک از #C داشته باشی که بخواهی با یک نفر به اشتراک بگذاری؟
اما ساختن یک Repository کامل برایش زیادی باشد؟
اینجاست که GitHub Gist وارد می‌شود. 🚀
ءGist در اصل یک راه ساده برای اشتراک‌گذاری Code Snippet و فایل‌های متنی است؛ اما یک نکته مهم دارد:
هر Gist در واقع یک Git Repository است.

یعنی برخلاف چیزی که شاید در نگاه اول به نظر برسد، فقط یک Text Box ساده برای Paste کردن کد نیست. Gist می‌تواند:
🔹 ءCommit History داشته باشد
🔹 ءDiff تغییرات را نگه دارد
🔹 ءClone شود
🔹 ءFork شود
🔹 چند فایل داشته باشد
🔹 و حتی داخل Blog یا Website Embed شود.
🎯 چه زمانی Gist واقعاً کاربردی است؟

فرض کن در یک پروژه NET. یک Extension Method نوشته‌ای:
public static bool IsValidEmail(this string value)
{
return value.Contains('@');
}

اگر بخواهی این را با همکارت به اشتراک بگذاری، احتمالاً یک Repository ساختن برای چنین قطعه کدی منطقی نیست.
می‌توانی آن را داخل یک Gist قرار بدهی و لینک همان Gist را ارسال کنی.
یا مثلاً:
📌 یک Regex پیچیده
📌 یک SQL Query
📌 یک Docker Command
📌 یک GitHub Actions Snippet
📌 یک نمونه appsettings.json
📌 یک الگوریتم کوچک
📌 یک قطعه کد برای پاسخ Stack Overflow
اینجا Gist بسیار مناسب است.

🆚 ءGist یا Repository؟

این دو را نباید جایگزین یکدیگر بدانیم. Repository برای یک پروژه یا Codebase کامل است.
مثلاً:
MyShop
├── src
├── tests
├── docs
├── .github
├── Dockerfile
└── README.md

اما Gist بیشتر برای چیزی شبیه این است:
retry-policy.cs

یا:
docker-compose.yml

یا حتی:
useful-sql-query.sql

یعنی وقتی Context و ساختار یک پروژه کامل لازم نیست، Gist انتخاب ساده‌تری است.
🧠 اما یک نکته مهم درباره Secret Gist!
اینجا یکی از رایج‌ترین سوءتفاهم‌ها وجود دارد. GitHub دو نوع Gist دارد:

🟢 Public Gist
در Discover نمایش داده می‌شود و قابل Search است.
🟡 Secret Gist
در Discover نمایش داده نمی‌شود و به‌صورت عمومی Search نمی‌شود.
اما...
ءSecret به معنی Private نیست. ❗️
اگر لینک Secret Gist را برای کسی بفرستی، او می‌تواند آن را ببیند.
اگر شخص دیگری somehow به URL دسترسی پیدا کند، او هم می‌تواند محتوا را مشاهده کند.
بنابراین:
Secret Gist
≠
Private Repository

اگر اطلاعات واقعاً حساس هستند، مثل:
API Key
Password
Connection String
Private Certificate
Access Token

نباید آن‌ها را داخل Secret Gist قرار دهید. GitHub هم صراحتاً توصیه می‌کند برای محتوایی که باید از دیگران مخفی بماند، از Private Repository استفاده کنید.

🔥 چیزی که Gist را جالب می‌کند

ءGist فقط Share کردن یک فایل نیست.
چون Git Repository است، می‌توانی آن را Clone کنی:
git clone https://gist.github.com/<gist-id>.git

بعد:
git add .
git commit -m "Improve retry example"
git push

یعنی می‌توانی یک Snippet را مثل یک Repository کوچک مدیریت کنی.
حتی GitHub برای Gist امکان مشاهده Revision History و Diff را هم فراهم می‌کند.
🛠 حتی از Terminal هم می‌توانی Gist بسازی
اگر GitHub CLI نصب باشد:
gh gist create example.cs

برای Public کردن:
gh gist create --public example.cs

و اگر بخواهی چند فایل را همزمان قرار دهی:
gh gist create example.cs example.sql docker-compose.yml

ءGitHub CLI به‌صورت پیش‌فرض Gist را Secret ایجاد می‌کند و برای Public کردن باید public-- را مشخص کنی.
حتی می‌توانی یک Gist را Clone کنی:
gh gist clone <gist-id>

و بعد مانند یک Git Repository معمولی روی آن کار کنی.

🌐 یک کاربرد جالب دیگر: Embed کردن
فرض کن یک Blog فنی داری و می‌خواهی کد را مستقیماً از GitHub نمایش بدهی.
می‌توانی Gist را داخل Blog Embed کنی.
در نتیجه وقتی کد داخل Gist تغییر کند، همان محتوای Gist می‌تواند در محل Embed شده نمایش داده شود.
این قابلیت برای:
📚 ءTutorialها
📝 مقالات فنی
💻 ءCode Exampleها
🎓 آموزش‌ها
خیلی کاربردی است.

💡 پس Gist را این‌طور به خاطر بسپار:
• Repository
برای ساختن و نگهداری یک پروژه.
• Gist
برای نگهداری و اشتراک‌گذاری یک قطعه کوچک و مستقل از کد یا متن.
و مهم‌تر از همه:
ءGist یک Pastebin ساده نیست؛ یک Git Repository کوچک برای Snippetهاست. 🚀
اگر تا امروز برای هر تکه کد کوچک یک Repository ساخته‌ای، شاید وقتش رسیده Gist را هم وارد جعبه‌ابزار GitHub خودت کنی. 😄

🔖هشتگ‌ها:
#GitHub #Git #Gist #GitHubTips #DeveloperTools
🎯 ایده اصلی کتاب چیست؟

مهم‌ترین ایده کتاب این است که وقتی یک سیستم باید سریع و قابل پیش‌بینی باشد، صرفاً سریع‌تر کردن کد کافی نیست.
در یک سیستم Low-Latency، باید کل مسیر درخواست را ببینی:
Request → Network → CPU → Memory → Application → I/O → Response

گاهی مشکل اصلاً در Algorithm نیست.
ممکن است چند میکروثانیه را در کد ذخیره کنی، اما چند میلی‌ثانیه را در Network، GC، Context Switch یا I/O از دست بدهی.
بنابراین کتاب تلاش می‌کند نگاه مهندسی به Latency را از سطح «این کد کند است» به سطح «دقیقاً کجای مسیر زمان از دست می‌رود؟» ببرد.
📚 کتاب چه چیزهایی را آموزش می‌دهد؟

🌐 1️⃣ شناخت Latency
اول باید بفهمیم Latency دقیقاً چیست و چرا با Throughput یکی نیست.

🧭 2️⃣ اندازه‌گیری Latency
قبل از Optimization باید بتوانی Latency را Measure کنی؛ نه اینکه صرفاً حدس بزنی مشکل کجاست.

⚡️3️⃣ Low-Latency Programming
چگونه تصمیم‌های داخل Application می‌توانند روی زمان پاسخ تأثیر بگذارند.

🧠4️⃣ CPU و پردازش
ءCPU، Cache، Instructionها و نحوه اجرای کد می‌توانند بخشی از Latency باشند.

💾5️⃣ Memory
دسترسی به Memory همیشه یکسان نیست و رفتار Cache و Memory Access می‌تواند روی Performance تأثیر جدی بگذارد.

🗑6️⃣ Garbage Collection
در Runtimeهایی که Garbage Collection دارند، Allocation و GC می‌توانند روی Latency و مخصوصاً Tail Latency تأثیر بگذارند.

🌐7️⃣ Network Latency
گاهی Application سریع است، اما شبکه کند است.
و در سیستم‌های Distributed، یک Request ممکن است چندین Network Hop داشته باشد.

💽8️⃣ I/O
ءDisk، Network و سایر I/Oها می‌توانند بخش قابل‌توجهی از زمان اجرای یک عملیات را مصرف کنند.

📊9️⃣ Tail Latency
ءAverage Latency به‌تنهایی تصویر درستی از سیستم نمی‌دهد.
ممکن است Average بسیار خوب باشد، اما تعداد کمی از Requestها Latency بسیار بالایی داشته باشند.
برای همین Percentileهایی مثل:
p50 → p95 → p99 → p99.9

در سیستم‌های حساس اهمیت زیادی پیدا می‌کنند.

🔬🔟 Performance Measurement
ءPerformance Optimization بدون Measurement می‌تواند بهینه‌سازی قسمت اشتباه سیستم باشد.

💡 یک نکته مهم کتاب

یکی از طرز فکرهای مهم در بحث Latency این است:
Don't optimize what you haven't measured.

اگر نمی‌دانی Latency کجا ایجاد می‌شود، تغییر دادن کد الزاماً Performance را بهتر نمی‌کند.
ممکن است Developer یک Method را بهینه کند و چند درصد CPU کمتر مصرف شود، در حالی که ۹۰٪ زمان Request در یک Network Call یا Database Query سپری می‌شود.
🏗 این کتاب برای چه کسی مناسب است؟

اگر فقط با CRUD Applicationهای ساده کار می‌کنی، احتمالاً بخش زیادی از مباحث کتاب برایت بیش از نیاز روزمره است.
اما اگر روی این حوزه‌ها کار می‌کنی، کتاب بسیار ارزشمندتر می‌شود:
🚀 High-Performance Systems
🌐 Distributed Systems
📡 Networking
💾 Databases
⚡️ Low-Latency Applications
📈 High-Traffic Services
💳 Trading & Financial Systems
🎮 Real-Time Systems
🧠 Performance Engineering
📌 در یک جمله

اگر بخواهم ایده اصلی کتاب را خیلی خلاصه کنم:
ءLatency را نمی‌توان فقط با سریع‌تر نوشتن Code حل کرد؛ باید کل مسیر اجرای یک Request را ببینی، اندازه‌گیری کنی و بفهمی زمان دقیقاً کجا مصرف می‌شود.
و شاید مهم‌ترین تغییر نگرشی که این کتاب ایجاد می‌کند همین باشد:
ءPerformance یک Feature نیست؛ یک خاصیت کل سیستم است.
🎯 در System Design اول Kafka و RabbitMQ را انتخاب نکن!

فرض کنید کاربر روی دکمه ثبت سفارش کلیک می‌کند.
در چند ثانیه آینده ممکن است ده‌ها اتفاق مختلف در سیستم رخ دهد:
🟢 پرداخت سفارش
🟢 بررسی و کسر موجودی
🟢 ارسال Email
🟢 ارسال SMS
🟢 ثبت امتیاز کاربر
🟢 بروزرسانی Analytics
🟢 ارسال Event برای Recommendation Engine
🟢 به‌روزرسانی سیستم Loyalty
حالا یک سؤال مهم:
آیا همه این عملیات باید قبل از اینکه به کاربر پاسخ 200 OK بدهیم، انجام شوند؟
اگر جواب «بله» باشد، خیلی سریع به یک مشکل معماری می‌رسیم.

🔴 اشتباه رایج

ممکن است چنین Flowای طراحی کنیم:
Client
↓
Order Service
↓
Payment Service
↓
Inventory Service
↓
Email Service
↓
SMS Service
↓
Analytics Service
↓
Recommendation Service
↓
Response

در ظاهر ساده است.
اما حالا فرض کنید:
Payment → 200ms
Inventory → 100ms
Email → 500ms
SMS → 800ms
Analytics → 1s
Recommendation → 2s

کاربر برای انجام یک عملیات ساده باید منتظر مجموع این وابستگی‌ها بماند.
بدتر از آن، اگر فقط Recommendation Service از دسترس خارج شود، آیا واقعاً باید ثبت سفارش کاربر هم Fail شود؟
احتمالاً نه.
اینجا یک مفهوم بسیار مهم وارد طراحی می‌شود:
Business Criticality
یعنی:
اگر این عملیات انجام نشود، آیا Business Rule اصلی نقض می‌شود؟


🟢 عملیات Business-Critical
مثلاً:
Payment
اگر پرداخت انجام نشود، نمی‌توانیم سفارش را به وضعیت Paid ببریم.
یا:
Inventory
اگر آخرین واحد محصول هم‌زمان به دو نفر فروخته شود، یک Business Invariant نقض شده است.
بنابراین این عملیات‌ها معمولاً بخشی از Critical Path هستند.
Create Order
↓
Check Inventory
↓
Reserve Stock
↓
Process Payment
↓
Order Confirmed

در این قسمت، Consistency و نتیجه قطعی عملیات اهمیت زیادی دارد.

🔵 اما همه چیز Business-Critical نیست

فرض کنید سفارش با موفقیت ثبت شده است.
حالا باید:
📧 ءEmail ارسال شود.
📱 ءSMS ارسال شود.
📊 اطلاعات Analytics ثبت شود.
⭐️ امتیاز کاربر محاسبه شود.
🤖 ءRecommendation Engine از خرید جدید مطلع شود.
آیا اگر Email Service برای ۳۰ ثانیه Down باشد، باید Order هم Fail شود؟
معمولاً نه.
این عملیات‌ها را می‌توان از مسیر اصلی خارج کرد.
در اینجا Asynchronous Processing می‌تواند انتخاب مناسب‌تری باشد.
اما اینجا یک نکته مهم وجود دارد...
وقتی می‌گوییم:
«این را Async کنیم»

هنوز نگفته‌ایم Kafka یا RabbitMQ.
چون انتخاب Broker باید بعد از مشخص شدن Communication Pattern انجام شود.
🐇 چه زمانی RabbitMQ منطقی‌تر است؟

فرض کنید بعد از ثبت سفارش، یک Task داریم:
SendOrderConfirmationEmail

این Message باید توسط یک Consumer پردازش شود.
ما الزاماً به تاریخچه بلندمدت Eventها، Replay کردن میلیون‌ها Event یا Stream Processing نیاز نداریم.
هدف ما بیشتر این است:
یک Message را قابل‌اعتماد به Consumer برسانیم و آن را پردازش کنیم.

در چنین سناریویی، یک Message Broker مانند RabbitMQ می‌تواند انتخاب مناسبی باشد.
ءConsumer می‌تواند Message را پردازش کند و در صورت موفقیت Ack بدهد.

🟣 ءKafka چه زمانی معنا پیدا می‌کند؟

حالا سناریوی دیگری را تصور کنید.
وقتی Order ایجاد شد، می‌خواهیم چندین سیستم مستقل از آن مطلع شوند:
OrderCreated
│
▼
Kafka
│
┌────┼────┬───────────┐
▼ ▼ ▼ ▼
CRM Analytics Recommendation Loyalty

حالا هر Consumer می‌تواند جریان Eventهای خودش را مستقل دنبال کند.
ءAnalytics ممکن است Event را مصرف کند.
ءRecommendation هم همان Event را مصرف کند.
ءLoyalty هم همان Event را مصرف کند.
و در سناریوهایی که نیاز به نگهداری Eventها و Replay وجود دارد، Kafka مدل متفاوتی نسبت به یک Message Queue سنتی ارائه می‌کند.
بنابراین سؤال درست این نیست:
«Kafka بهتر است یا RabbitMQ؟»

سؤال درست این است:
«من دقیقاً چه نوع Communication Patternای دارم؟»

⚠️ یک اشتباه خطرناک‌تر
حتی اگر تصمیم بگیریم عملیات را Async کنیم، هنوز کار تمام نشده است.
مثلاً:
Order DB
↓
Publish Event

اگر Order در Database Commit شود ولی Publish کردن Event شکست بخورد چه؟
DB Commit ✅

Publish Event ❌

حالا Order ایجاد شده، اما هیچ Consumerای از آن خبر ندارد.
اینجاست که الگوهایی مثل Transactional Outbox اهمیت پیدا می‌کنند.
در این مدل، ایجاد Order و ثبت Event در Outbox می‌تواند در یک Database Transaction انجام شود.
بعد یک Worker، Event را از Outbox خوانده و به Broker منتشر می‌کند.
در نتیجه، دیگر به این شکل نیست که:
«اول Order را ذخیره کنیم و امیدوار باشیم بعداً Publish هم موفق شود.»


🎯 یک قانون ساده برای System Design
قبل از اینکه بگویید:
RabbitMQ
یا
Kafka
یا
gRPC
یا
Redis
یا هر تکنولوژی دیگری...
ابتدا این سه سؤال را بپرسید:
1️⃣ اگر این عملیات Fail شود، آیا Business Transaction باید Fail شود؟
اگر بله → احتمالاً Synchronous / Critical Path
اگر نه → احتمالاً می‌تواند Asynchronous شود.
2️⃣ آیا Consumer باید بلافاصله نتیجه را پردازش کند یا فقط باید از اتفاق رخ‌داده مطلع شود؟
این سؤال به شما کمک می‌کند بین Command-oriented Messaging و Event-oriented Communication تصمیم بگیرید.
3️⃣ آیا به قابلیت‌هایی مثل Retention، چند Consumer مستقل، Replay و Stream Processing نیاز دارید؟
اگر بله، انتخاب‌هایی مثل Kafka جدی‌تر می‌شوند.

🚀 در نهایت...

به نظرم یکی از مهم‌ترین مهارت‌های System Design این نیست که بتوانی ده‌ها تکنولوژی را نام ببری.
مهم‌تر این است که بتوانی تشخیص بدهی:
کدام عملیات واقعاً باید همین الان انجام شود؟
و کدام عملیات می‌تواند چند ثانیه بعد انجام شود، بدون اینکه Business Rule اصلی سیستم نقض شود؟
وقتی این مرز را درست مشخص کردی، انتخاب تکنولوژی خیلی ساده‌تر می‌شود.
چون آن موقع دیگر نمی‌پرسی:
❌ Kafka یا RabbitMQ؟

بلکه می‌پرسی:
✅ ءBusiness من چه نوع Communication Patternای نیاز دارد؟

و این دقیقاً جایی است که System Design از انتخاب تکنولوژی جدا می‌شود.
#اشتباهات_مهندسی (Engineering Mistakes)
یک تیم ۵ نفره داشت یک Feature را توسعه می‌داد. Project Manager گفت:
«کار را تقسیم کنیم که سریع‌تر پیش بره.»
پس Feature را کردند ۱۰ تا Task:
Developer A → 2 Tasks
Developer B → 2 Tasks
Developer C → 2 Tasks
Developer D → 2 Tasks
Developer E → 2 Tasks

روی کاغذ عالی بود.
همه مشغول بودند.
اما سه روز بعد...
ءDeveloper A منتظر API بود.
ءDeveloper B منتظر Database Migration بود.
ءDeveloper C منتظر تصمیم Backend بود.
ءDeveloper D کار خودش را تمام کرده بود، اما نمی‌توانست Merge کند.
و Developer E داشت روی چیزی کار می‌کرد که بعداً مشخص شد دیگر لازم نیست.
همه Busy بودند.
اما Feature جلو نمی‌رفت.
مشکل کجا بود؟
تیم کار را بر اساس تعداد Task تقسیم کرده بود،
نه بر اساس Dependency.
در واقع جریان کار این شکلی بود:
Database
↓
Backend
↓
API Contract
↓
Frontend
↓
Integration

ولی ما وانمود کرده بودیم همه این کارها مستقل‌اند.
یکی از خطرناک‌ترین اشتباهات در مدیریت Engineering همین است:
ءBusy بودن را با Progress اشتباه بگیریم.
ممکن است ۱۰ نفر همزمان روی ۱۰ Task کار کنند،
اما اگر ۹ تای آن‌ها منتظر یک Task باشند،
سرعت واقعی تیم همان سرعت آن یک Task است.
گاهی برای سریع‌تر شدن پروژه،
نباید کار بیشتری بین افراد پخش کنیم.
باید Dependencyهای اصلی را پیدا کنیم.
چون در Software Engineering،
تعداد Taskهای انجام‌شده مهم نیست.
مهم این است که:
چه مقدار از مسیر رسیدن به یک نتیجه واقعی طی شده است.
🖼 کار با ImageMagick در C#؛ وقتی System.Drawing دیگر کافی نیست

اگر در یک پروژه NET. با تصویرها کار کرده باشید، احتمالاً خیلی زود به این نیازها می‌رسید:
🔹 تغییر اندازه تصاویر
🔹 تبدیل فرمت JPEG، PNG، WebP و ...
🔹 ءCrop کردن تصویر
🔹 فشرده‌سازی
🔹 ساخت Thumbnail
🔹 چرخاندن یا تغییر Orientation
🔹 اضافه کردن Watermark
🔹 پردازش تصاویر در سمت Server
🔹 تولید چند نسخه از یک تصویر با اندازه‌های مختلف
در پروژه‌های ساده شاید بتوانید با APIهای معمول NET. بخش زیادی از این کارها را انجام دهید؛ اما وقتی پردازش تصویر جدی‌تر می‌شود، معمولاً به یک کتابخانه قدرتمندتر نیاز دارید.
اینجاست که ImageMagick وارد می‌شود. 🧰
ءImageMagick یک مجموعه ابزار و کتابخانه قدرتمند برای پردازش تصاویر است که از فرمت‌های بسیار متنوعی پشتیبانی می‌کند و در دنیای NET. نیز می‌توان از طریق کتابخانه‌هایی مانند Magick.NET با آن کار کرد.
🤔 ءImageMagick دقیقاً چه مشکلی را حل می‌کند؟

فرض کنید در یک سایت، کاربر یک تصویر با حجم 8 MB و ابعاد 6000 × 4000 آپلود می‌کند.
شما نمی‌خواهید همان فایل را مستقیماً برای همه کاربران سرو کنید.
ممکن است لازم باشد:
📌 نسخه اصلی را نگه دارید.
📌 یک نسخه 1920px برای Desktop بسازید.
📌 یک نسخه 800px برای Mobile بسازید.
📌 یک Thumbnail با ابعاد 300 × 300 ایجاد کنید.
📌 تصاویر را به WebP تبدیل کنید.
📌 حجم خروجی را کاهش دهید.
📌 ءMetadata غیرضروری را حذف کنید.
یعنی یک فایل ورودی دارید اما چندین خروجی مختلف تولید می‌کنید.
ءImageMagick دقیقاً برای چنین پردازش‌هایی بسیار قدرتمند است.
🚀 نصب در پروژه #C

در پروژه NET. می‌توانید از Magick.NET استفاده کنید.
برای مثال:
dotnet add package Magick.NET-Q8-AnyCPU

بعد:
using ImageMagick;

حالا می‌توانیم یک تصویر را Load کنیم:
using var image = new MagickImage("input.jpg");

Console.WriteLine(image.Width);
Console.WriteLine(image.Height);

📐 تغییر اندازه تصویر
یکی از رایج‌ترین عملیات‌ها:
using var image = new MagickImage("input.jpg");

image.Resize(1200, 0);

image.Write("output.jpg");

در اینجا عرض تصویر به 1200px تغییر می‌کند و ارتفاع متناسب با نسبت تصویر محاسبه می‌شود.
این موضوع برای ساخت نسخه‌های مختلف تصاویر بسیار کاربردی است.
مثلاً:
Original
│
├── 1920px
├── 1200px
├── 800px
└── 300px Thumbnail

✂️ ؟Crop کردن تصویر
مثلاً می‌خواهیم بخشی از تصویر را جدا کنیم:
using var image = new MagickImage("input.jpg");

image.Crop(new MagickGeometry(300, 300)
{
IgnoreAspectRatio = true
});

image.Write("cropped.jpg");

در پروژه‌های واقعی می‌توانید از این قابلیت برای تولید Avatar، Thumbnail و تصاویر Preview استفاده کنید.
🔄 تبدیل فرمت
یکی از قابلیت‌های مهم ImageMagick تبدیل فرمت تصاویر است.
مثلاً:
using var image = new MagickImage("input.jpg");

image.Format = MagickFormat.WebP;

image.Write("output.webp");

یعنی:
JPEG
↓
ImageMagick
↓
WebP

این موضوع مخصوصاً برای Web Applicationها مهم است، چون می‌توانید نسخه‌ای مناسب برای نمایش در Web تولید کنید و حجم انتقال داده را کاهش دهید.
🗜 کنترل کیفیت و حجم
می‌توان کیفیت خروجی را نیز کنترل کرد:
using var image = new MagickImage("input.jpg");

image.Quality = 80;

image.Write("compressed.jpg");

اما یک نکته مهم وجود دارد:
ءQuality پایین‌تر همیشه به معنی نتیجه بهتر نیست.
باید بین:
Image Quality
↕️
File Size
↕️
Network Cost
↕️
Storage Cost

تعادل برقرار کنید.
💧 اضافه کردن Watermark
فرض کنید یک سیستم مدیریت تصاویر دارید و می‌خواهید روی تصاویر کاربران Watermark قرار دهید. ImageMagick امکان Composite کردن تصاویر را نیز فراهم می‌کند.
برای مثال:
using var image = new MagickImage("photo.jpg");
using var watermark = new MagickImage("watermark.png");

image.Composite(
watermark,
Gravity.Southeast,
CompositeOperator.Over);

image.Write("result.jpg");

حالا Watermark در گوشه تصویر قرار می‌گیرد.
🏢 یک مثال واقعی در پروژه‌های بزرگ
فرض کنید یک E-Commerce دارید که روزانه هزاران تصویر محصول دریافت می‌کند.
کاربر ممکن است تصویری با مشخصات زیر Upload کند:
File Size: 12 MB
Resolution: 8000 × 6000
Format: JPEG

اشتباه این است که همین فایل را مستقیماً در Response به Browser برگردانیم.
بهتر است Pipeline پردازش تصویر داشته باشیم:
              User Upload
│
▼
Object Storage
│
▼
Image Processing
│
┌────────┼────────┐
▼ ▼ ▼
1920px 800px 300px
│ │ │
└────────┼────────┘
▼
WebP / AVIF
│
▼
CDN / Storage

در این معماری، API اصلی نباید مجبور باشد تمام پردازش تصویر را در همان Request انجام دهد.
برای تصاویر بزرگ یا پردازش‌های سنگین می‌توان:
Upload
↓
Store Original
↓
Publish Event / Message
↓
Image Processing Worker
↓
ImageMagick
↓
Generate Variants
↓
Object Storage

را پیاده‌سازی کرد.
این طراحی باعث می‌شود Upload کاربر سریع‌تر پاسخ داده شود و پردازش سنگین تصویر به Worker منتقل شود. ⚙️
⚠️ اما یک نکته بسیار مهم

ءImageMagick ابزار قدرتمندی است؛ اما هر پردازش تصویری را نباید داخل Web Request انجام دهید.
فرض کنید کاربر یک تصویر بسیار بزرگ Upload کرده است.
اگر در همان Request:
Load Image
↓
Resize
↓
Crop
↓
Convert
↓
Compress
↓
Save

را انجام دهید، با افزایش همزمانی ممکن است CPU و Memory سرور به‌شدت مصرف شوند.
به همین دلیل در سیستم‌های پرترافیک بهتر است:
API
│
├── Validate
├── Store
└── Queue Message
│
▼
Image Worker
│
▼
ImageMagick

داشته باشیم.
🔐 یک موضوع امنیتی بسیار مهم

هر فایل تصویری که کاربر Upload می‌کند، الزاماً قابل اعتماد نیست.
نباید صرفاً به این اعتماد کنیم:
Content-Type: image/jpeg

یا:
file.jpg

در یک سیستم واقعی باید Upload Validation، محدودیت اندازه فایل، محدودیت ابعاد تصویر و سیاست‌های مناسب برای فرمت‌های مجاز داشته باشیم.
همچنین ImageMagick را نباید با تنظیمات ناامن و بدون توجه به سیاست‌های امنیتی محیط Production اجرا کرد.
یعنی:
ءImage Processing فقط یک مسئله فنی مربوط به Resize کردن تصویر نیست؛ بخشی از Security Pipeline سیستم هم محسوب می‌شود. 🔐


⚖️ ءImageMagick را چه زمانی استفاده کنیم؟ ImageMagick انتخاب بسیار خوبی است وقتی که:
✅ فرمت‌های تصویری متنوع دارید.
✅ عملیات پیچیده Image Processing دارید.
✅ به Resize، Crop، Composite، Conversion و Compression نیاز دارید.
✅ سیستم شما باید تعداد زیادی تصویر را پردازش کند.
✅ نیاز دارید یک Processing Pipeline مستقل برای تصاویر داشته باشید.
✅ می‌خواهید پردازش تصویر را به Worker منتقل کنید.
❌ چه زمانی شاید انتخاب مناسبی نباشد؟
اگر فقط می‌خواهید:
یک تصویر ساده Upload کنید
↓
Resize ساده
↓
Save

استفاده از یک کتابخانه سنگین ممکن است بیش از نیاز شما باشد.
همچنین اگر Processing شما کاملاً وابسته به قابلیت‌های خاص GPU یا یک Pipeline تخصصی Computer Vision باشد، ImageMagick الزاماً بهترین ابزار نیست.
برای بعضی سناریوها کتابخانه‌های تخصصی‌تر انتخاب بهتری هستند.
🧠 یک اشتباه معماری رایج
این کد:
public async Task<IActionResult> Upload(IFormFile file)
{
using var image = new MagickImage(file.OpenReadStream());

image.Resize(1200, 0);

image.Write("output.webp");

return Ok();
}

ممکن است در یک پروژه کوچک کاملاً قابل قبول باشد.
اما اگر همین API:
1000 concurrent uploads

دریافت کند، داستان کاملاً متفاوت می‌شود.
پس سؤال اصلی این نیست که:
«آیا ImageMagick سریع است؟»

سؤال معماری مهم‌تر این است:
«آیا Image Processing باید در Request/Response Lifecycle من اتفاق بیفتد؟»
در سیستم‌های بزرگ، پاسخ خیلی وقت‌ها خیر است.
🎯 جمع‌بندی

ءImageMagick فقط یک ابزار برای تبدیل JPG به PNG نیست.
می‌تواند بخشی از یک Image Processing Pipeline باشد.
برای مثال:
Upload
↓
Validation
↓
Object Storage
↓
Message Queue
↓
Image Worker
↓
ImageMagick
↓
Resize / Crop / Compress / Convert
↓
Generate Variants
↓
Object Storage
↓
CDN

و اینجاست که استفاده از آن در یک پروژه NET. واقعاً معنا پیدا می‌کند.
اگر فقط یک Resize ساده دارید، ساده نگهش دارید.
اما اگر با یک سیستم واقعی و پرتعداد از تصاویر سروکار دارید، ImageMagick + Worker + Object Storage + CDN می‌تواند یک ترکیب بسیار قدرتمند برای ساخت یک Image Processing Pipeline باشد. 🚀

🔖هشتگ‌ها:
#aspnetcore #imagemagick #imageprocessing
📌پیاده‌سازی اصولی لغو عملیات (Cancellation Support) در تمام لایه‌های دات‌نت

نادیده گرفتن الگوی لغو عملیات یکی از شایع‌ترین نقص‌ها در پروژه‌های دات‌نت است. هنگامی که کاربری تب مرورگر را می‌بندد یا کلاینت درخواست HTTP را متوقف می‌کند، در صورت عدم انتشار توکن لغو (CancellationToken)، سرور همچنان به اجرای کوئری‌های سنگین دیتابیس، پردازش‌های CPU و فراخوانی‌های I/O ادامه می‌دهد که نتیجه آن هدررفت مستقیم منابع و اشباع Thread Pool است. استفاده از Copilot برای افزودن قابلیت Cancellation نباید به «لغو ظاهری یا نمادین» (Fake Cancellation) با چک کردن دستی توکن منتهی شود، بلکه باید انتشار پیوسته و معنادار توکن در تمام لایه‌ها تا پایین‌ترین سطح I/O را پیاده‌سازی کند.

کالبدشکافی لغو نامعتبر در برابر لغو واقعی

رویکرد اشتباه (لغو ظاهری): 
قرار دادن متوالی cancellationToken.ThrowIfCancellationRequested() در متدها، بدون ارسال توکن به عملیات پایه‌ای دیتابیس یا سوکت شبکه. در این حالت، تا زمانی که کوئری دیتابیس تمام نشود، برنامه متوجه توقف درخواست نمی‌شود.
رویکرد اصولی (لغو واقعی در سطح سخت‌افزار/درایور): ارسال توکن به درایور دیتابیس (مانند Npgsql یا Microsoft.Data.SqlClient) تا به محض لغو درخواست کلاینت، دستور ATTN یا لغو سوکت به سرور دیتابیس فرستاده شده و پردازش همان‌جا متوقف شود.

ساختار پرامپت مهندسی برای ردیابی و افزودن CancellationToken
برای گسترش ایمن توکن لغو در لایه‌های معماری:
ساختار لغو عملیات (CancellationToken) را در تمام زنجیره این سناریو از لایه Controller تا پایگاه داده پیاده‌سازی کن.


الزامات و محورهای ردیابی:
1️⃣دریافت توکن از درخواست HTTP کلاینت در لایه اکشن کنترلر / Minimal API.

2️⃣انتقال مستقیم توکن از طریق Interfaceهای لایه سرویس و ریپازیتوری.

3️⃣اعمال مستقیم توکن روی درایور و متدهای ناهمگام EF Core (مانند ToListAsyncیا SaveChangesAsync)
4️⃣پرهیز از لغو ظاهری (Fake Cancellation) و توضیح دقیق نقطه‌ای که لغو به صورت فیزیکی عملیات I/O را قطع می‌کند.

5️⃣مدیریت تمیز استثنای OperationCanceledException بدون لاگ‌های خطای بحرانی نامرتبط.

ردیابی گام‌به‌گام در لایه‌های مختلف معماری دات‌نت

۱. لایه Controller / Presentation
کنترلر فریم‌ورک ASP.NET Core به طور خودکار HttpContext.RequestAborted را به پارامترهای از نوع CancellationToken متصل (Bind) می‌کند:
[HttpGet]
public async Task<ActionResult<IReadOnlyList<OrderResponseDto>>> GetOrdersAsync(
CancellationToken cancellationToken)
{
var orders = await orderService.GetOrdersAsync(cancellationToken);
return Ok(orders);
}


۲. لایه Service و Repository
توکن باید بدون دستکاری و ایجاد State اضافی از امضای متدها عبور کند:
public interface IOrderRepository
{
Task<IReadOnlyList<Order>> GetOrdersAsync(CancellationToken cancellationToken);
}

public sealed class OrderRepository(ApplicationDbContext dbContext) : IOrderRepository
{
public async Task<IReadOnlyList<Order>> GetOrdersAsync(CancellationToken cancellationToken)
{
// نقطه اثر واقعی: توکن به صورت مستقیم به درایور پایگاه داده فرستاده می‌شود
return await dbContext.Orders
.AsNoTracking()
.ToListAsync(cancellationToken);
}
}


نکات تکمیلی برای سناریوهای پیشرفته لغو عملیات

ترکیب توکن‌ها با CancellationTokenSource.CreateLinkedTokenSource: در پردازش‌های پس‌زمینه یا فراخوانی وب‌سرویس‌ها، گاهی نیاز به اعمال Timeout مشخص در کنار درخواست توقف کاربر وجود دارد:
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);

await externalClient.FetchDataAsync(linkedCts.Token);

مدیریت لاگ‌ها در زمان لغو: استثنای OperationCanceledException یا TaskCanceledException یک رفتار طبیعی و مورد انتظار است، نه یک باگ غیرمنتظره (Crash). به Copilot تأکید کنید که هنگام مدیریت خطا در Middlewareها، لغو درخواست را به عنوان خطای سطحی (LogInformation یا LogDebug) لاگ کند، نه به عنوان خطای بحرانی سطح LogError.
عملیات بحرانی غیرقابل لغو: در تراکنش‌های مالی یا ثبت لاگ‌های حساس که پس از شروع نباید متوقف شوند، از CancellationToken.None استفاده کنید تا لغو سمت کاربر باعث ناتمام ماندن ثبت اسناد نشود.

قاعده کلیدی: انتشار توکن لغو باید پیوسته و بدون وقفه باشد؛ هدف لغو عملیات، آزادسازی فوری سوکت‌ها، اتصالات دیتابیس و منابع سخت‌افزاری در پایین‌ترین لایه I/O است.
#تصمیم‌های_مهندسی (Engineering Decisions)
یکی از تصمیم‌هایی که تقریباً هر تیمی دیر یا زود با آن روبه‌رو می‌شود این است:
«آیا این عملیات باید Synchronous باشد یا Asynchronous؟»
خیلی‌ها به‌محض شنیدن کلمه Asynchronous تصور می‌کنند با انتخاب آن، سیستم سریع‌تر می‌شود.
اما سؤال اصلی چیز دیگری است.
آیا کاربر واقعاً لازم است منتظر بماند؟
فرض کنید کاربری سفارشی ثبت می‌کند.
بعد از ثبت سفارش باید:
ایمیل ارسال شود.
پیامک ارسال شود.
فاکتور تولید شود.
موجودی انبار به‌روزرسانی شود.
ءNotification برای مدیر ارسال شود.
آیا همه این کارها باید قبل از دریافت پاسخ HTTP انجام شوند؟
اگر جواب نه باشد، شاید نگه داشتن کاربر پشت این عملیات، فقط Latency سیستم را بیشتر کرده باشد.
اما طرف دیگر ماجرا هم مهم است.
اگر همه چیز را Asynchronous کنیم،
حالا باید با چالش‌های جدیدی کنار بیاییم:
پیام تکراری (Duplicate Messages)، ترتیب پیام‌ها (Ordering)، تحویل حداقل یک‌بار (At-Least-Once Delivery)، Idempotency، مانیتورینگ صف‌ها،
مدیریت خطا و Retry.
یعنی مسئله فقط عوض شده است؛ از بین نرفته.
به همین دلیل، مهندسان باتجربه اول از خودشان نمی‌پرسند:
«چطور این را Asynchronous کنیم؟»
بلکه می‌پرسند:
«کاربر واقعاً منتظر نتیجه این عملیات است یا نه؟»
چون در مهندسی نرم‌افزار، Synchronous و Asynchronous، خوب یا بد نیستند.
هر کدام، هزینه‌ها و مزایای خودشان را دارند.
و تصمیم درست،
همان تصمیمی است که با نیاز واقعی سیستم هم‌خوانی داشته باشد.
#فرهنگ_مهندسی (Engineering Culture)
در یک جلسه، تیم داشت درباره استفاده از یک تکنولوژی جدید تصمیم می‌گرفت. Tech Lead پرسید:
«همه موافقیم؟»
چند ثانیه سکوت.بعد همه گفتند:
«آره.»
تصمیم گرفته شد. چند هفته بعد، پروژه به مشکل خورد.
یکی گفت:
«راستش من از اول فکر می‌کردم این انتخاب مناسب نیست.»
یکی دیگر گفت:
«منم شک داشتم، ولی چون همه موافق بودن چیزی نگفتم.»
مشکل این نبود که تیم تصمیم اشتباهی گرفته بود.
مشکل این بود که کسی مخالفتش را وارد تصمیم نکرده بود.
سکوت همیشه به معنی موافقت نیست.
گاهی یعنی:
"شاید اشتباه می‌کنم."
"حوصله بحث ندارم."
"بقیه بیشتر می‌دونن."
"مخالفت کردن دردسر داره."

یک تیم مهندسی سالم فقط جایی نیست که آدم‌ها راحت نظر بدهند.
باید جایی باشد که بتوانند راحت بگویند:
«من قانع نشدم.»
گاهی یک سؤال مخالف می‌تواند جلوی هفته‌ها Refactor را بگیرد:
«اگر Database Down شد چی؟»
«اگر این API ده برابر Traffic بگیره چی؟»
«چرا اصلاً به این Microservice نیاز داریم؟»
تصمیم خوب از این نمی‌آید که همه سریع بگویند:
«موافقم.»
گاهی از جایی شروع می‌شود که یک نفر جرئت می‌کند بگوید:
«صبر کنید، فکر کنم داریم یک چیزی رو نمی‌بینیم.»
🚨 یک متد Async چطور می‌تواند کل برنامه را بدون هیچ Exceptionای قفل کند؟

یکی از عجیب‌ترین باگ‌هایی که ممکن است در پروژه‌های NET. با آن مواجه شوید این است:
برنامه اجرا می‌شود.
هیچ Exceptionای ندارید.CPU هم لزوماً بالا نیست.
اما یک بخش از برنامه دیگر جلو نمی‌رود.
همه‌چیز منتظر چیزی است...
و آن چیز هم منتظر همان Thread است که شما قفلش کرده‌اید.
به این وضعیت می‌گوییم:🔒 Async Deadlock
🤔 ءDeadlock دقیقاً چطور اتفاق می‌افتد؟

فرض کنید یک متد Async دارید:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000);

return "Hello";
}

و آن را این‌طور صدا می‌زنید:
var result = GetDataAsync().Result;

در ظاهر شاید مشکلی نبینید.
اما در محیط‌هایی که SynchronizationContext وجود دارد، اتفاق زیر ممکن است رخ دهد:
Thread اصلی
│
▼
GetDataAsync()
│
▼
await
│
├── متد کنترل را آزاد می‌کند
│
▼
ءContinuation باید روی Context اصلی ادامه پیدا کند
│
❌
اما Thread اصلی با Result. بلاک شده است
│
▼
Continuation نمی‌تواند اجرا شود
│
▼
.Result منتظر پایان Task است
│
🔒 DEADLOCK

یعنی:
ءTask منتظر Thread است
و Thread منتظر Task

و هیچ‌کدام نمی‌توانند جلو بروند.

☠️ ترکیب خطرناک: await + .Result

مشکل اصلی async نیست.
مشکل زمانی ایجاد می‌شود که:
یک عملیات Async دارید await می‌کنید Continuation باید به یک SynchronizationContext خاص برگردد
اما همان Context را با .Result یا .Wait() بلاک کرده‌اید
مثلاً:
var result = GetDataAsync().Result;

یا:
``lua
GetDataAsync().Wait();``


این دو مورد در چنین سناریوهایی می‌توانند یکی از رایج‌ترین دلایل Deadlock باشند.

🧠 نقش SynchronizationContext چیست؟
وقتی شما می‌نویسید:
await SomeAsyncOperation();

بعضی محیط‌های NET. تلاش می‌کنند بعد از پایان عملیات Async، ادامه متد را دوباره روی همان Context قبلی اجرا کنند.
مثلاً:
UI Thread
│
▼
await
│
▼
عملیات Async
│
▼
بازگشت به UI Thread

این رفتار در محیط‌هایی مثل:
WPF
Windows Forms
ASP.NET کلاسیک
اهمیت زیادی دارد.
چون ممکن است بعد از await نیاز داشته باشید دوباره به Context اصلی برگردید.
اما اگر همان Context را بلاک کنید:
.Result

ءContinuation جایی برای اجرا ندارد.
و برنامه وارد Deadlock می‌شود.

🛠 راهکار اول: Async All the Way
بهترین و مهم‌ترین راهکار این است که عملیات Async را دوباره به عملیات Sync تبدیل نکنید.
❌ این:
public string GetData()
{
return GetDataAsync().Result;
}

بهتر است به این تبدیل شود:
public async Task<string> GetDataAsync()
{
return await _service.GetDataAsync();
}

و در لایه بالاتر:
var result = await GetDataAsync();

قاعده ساده است:
اگر وارد دنیای Async شدید، تا جای ممکن در همان دنیا بمانید.

این همان چیزی است که معمولاً با عنوان:
🚀 Async All the Way
شناخته می‌شود.
🔧 ءConfigureAwait(false) چه کاری انجام می‌دهد؟
فرض کنید یک Library دارید:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000);

return "Hello";
}

به‌صورت پیش‌فرض، در محیط‌هایی که Context وجود دارد، await ممکن است تلاش کند ادامه اجرای متد را روی همان Context قبلی ادامه دهد.
اما با:
await Task.Delay(1000).ConfigureAwait(false);

به سیستم می‌گویید:
بعد از پایان این عملیات، لازم نیست اجرای ادامه متد را روی Context قبلی ادامه بدهی.

مثلاً:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000)
.ConfigureAwait(false);

return "Hello";
}

در نتیجه احتمال گرفتار شدن در Deadlockهای کلاسیک ناشی از SynchronizationContext کاهش پیدا می‌کند.
⚠️ اما آیا باید همه‌جا ConfigureAwait(false) بنویسیم؟
نه دقیقاً.
اگر در یک Application UI هستید و بعد از await می‌خواهید UI را تغییر دهید، ممکن است نیاز داشته باشید به همان Context اصلی برگردید.
مثلاً:
await LoadDataAsync();

// ممکن است نیاز باشد روی UI Thread باشیم
MyLabel.Text = "Loaded";

اما در Libraryها و لایه‌هایی که وابستگی مستقیمی به Context برنامه ندارند، معمولاً عدم وابستگی به Context باعث می‌شود کد قابل‌استفاده‌تر و مستقل‌تر باشد.
به همین دلیل در بسیاری از Libraryها و کدهای زیرساختی، استفاده از:
.ConfigureAwait(false)

یک الگوی رایج است.
📌 دستورالعمل عملی برای پروژه‌های واقعی
اگر می‌خواهید احتمال Deadlock و مشکلات ناشی از Sync-over-Async را کاهش دهید:
1️⃣ از .Result استفاده نکنید
غلط❌
var result = GetDataAsync().Result;

درست✅
var result = await GetDataAsync();


2️⃣ از .Wait() استفاده نکنید
غلط❌
GetDataAsync().Wait();

درست✅
csharp
await GetDataAsync();


3️⃣ ءAsync All the Way را رعایت کنید
اگر یک متد Async است:
Repository
↓
Service
↓
Handler
↓
Controller

اجازه دهید Task در تمام مسیر حرکت کند.
نه اینکه وسط مسیر ناگهان:
.Result

یا:
.Wait()

صدا زده شود.

4️⃣ در Libraryها به Context وابسته نشوید
اگر Library شما نیازی به Context اصلی ندارد، می‌توانید در نقاط مناسب از:
.ConfigureAwait(false)

استفاده کنید.

5️⃣ از async void فقط برای Event Handler استفاده کنید
تقریباً در تمام Serviceها، Repositoryها، Handlerها و سایر متدهای Async:
Task

یا:
Task<T>

انتخاب مناسب‌تری است.
🎯 جمع‌بندی

ءDeadlock در دنیای Async معمولاً از خود async/await شروع نمی‌شود.
مشکل زمانی ایجاد می‌شود که:
یک عملیات Async را مجبور کنیم به‌صورت Sync اجرا شود.
یعنی:

.Result


یا:

.Wait()


در محیطی که Continuation نیاز دارد دوباره روی یک Context بلاک‌شده اجرا شود.
راهکار اصلی؟
🚀 Async All the Way
و در صورت نیاز:
🔧 ConfigureAwait(false)
و مهم‌تر از همه:
ءAsync code را Sync نکنید فقط چون صبر کردن با await برایتان راحت‌تر نیست.

در ASP.NET Core شاید Deadlock کلاسیک SynchronizationContext را کمتر ببینید، اما Sync-over-Async همچنان می‌تواند Thread Pool را اشباع کند و سیستم شما را زیر Load به زانو دربیاورد.

🔖هشتگ‌ها:
#dotnet #csharp #async #await #deadlock #aspnetcore #threadpool
#روایت_تجربه
یه چیزی که کم‌کم داره توی کارم تغییر می‌کنه، تعریفم از «تموم شدن کاره».
قبلاً وقتی یک Task رو می‌بستم، حس می‌کردم کار تموم شده.
کد نوشته شده بود. تست‌ها پاس شده بودند. PR باز شده بود.Done. ✅
ولی چند وقتیه بیشتر به این فکر می‌کنم که:
واقعاً Done یعنی چی؟

چند بار شده یک Feature رو Merge کنیم و دو هفته بعد تازه بفهمیم هنوز کار داریم؟
چون: Monitoring نداره.Error Scenarioهاش مشخص نیست.Rollback براش در نظر نگرفتیم. Documentationش ناقصه.
یا بدتر از همه...
کسی غیر از کسی که نوشته، واقعاً نمی‌دونه چطور کار می‌کنه.
از اون طرف، بعضی وقت‌ها یک نفر چند روز روی یک Feature کار می‌کنه، PR رو Merge می‌کنه و می‌ره سراغ Task بعدی.
اما همون Feature از روز اول وارد Production شده و هیچ‌کس مجبور نشده دوباره سمتش بره.
نه Bug. نه Hotfix.نه جلسه اضطراری.
نه پیام ساعت ۲ نصف شب. 😅
به نظرم دومی خیلی بیشتر شبیه «کار تموم‌شده» است.
شاید یکی از بدترین چیزهایی که توی Software Engineering یاد گرفتیم اینه که Progress رو با تعداد Taskهای Done اندازه بگیریم.
در حالی که بعضی Taskها وقتی Done می‌شن، تازه هزینه‌شون شروع می‌شه.
کد نوشته شده.
ولی نگهداریش شروع شده. Feature Deploy شده.
ولی رفتار واقعیش تازه مشخص می‌شه.
سیستم ساخته شده.
ولی حالا باید سال‌ها باهاش زندگی کنیم.
برای همین این روزها سعی می‌کنم کمتر بپرسم:
«کی تمومش می‌کنیم؟»
و بیشتر بپرسم:
«بعد از اینکه تموم شد، چه چیزی ازش باقی می‌مونه؟»
چون در نهایت، کد خوب فقط کدی نیست که امروز Merge بشه.
کدی‌ه که فردا کسی بابت Merge کردنش پشیمون نشه.
#AI_Software_Engineering
یه چیزی که این روزها بیشتر از خود AI ذهنم رو درگیر کرده، اینه که هزینه‌ی تغییر دادن کد داره به‌شدت کم میشه.
قبلاً وقتی می‌خواستی یک تغییر نسبتاً بزرگ روی یک سیستم انجام بدی، اول باید با خودت کنار می‌اومدی که:
چند روز باید Codebase رو بخونم؟
کدوم قسمت‌ها تحت تأثیر قرار میگیرن؟
چقدر Refactor لازمه؟
چندتا Test ممکنه بشکنه؟
و اصلاً ارزشش رو داره یا نه؟
همین هزینه باعث می‌شد خیلی از تغییرها اتفاق نیفتن.
نه لزوماً چون تصمیم اشتباهی بودن.
چون هزینه‌ی بررسی و انجامشون زیاد بود.
الان اما یه Codebase بزرگ رو میدی به یک Agent و میگی:
«این بخش رو بررسی کن، Dependencyهاش رو پیدا کن، این تغییر رو اعمال کن و Testها رو هم درست کن.»
چند دقیقه بعد، یک PR آماده داری.
و به نظرم اینجا یک خطر جدید به وجود میاد.
قبلاً یکی از سؤال‌های اصلی این بود:
«آیا واقعاً می‌تونیم این تغییر رو انجام بدیم؟»
الان بیشتر باید بپرسیم:
«آیا واقعاً باید این تغییر رو انجام بدیم؟»
چون وقتی هزینه‌ی تغییر پایین میاد، وسوسه‌ی تغییر دادن همه‌چیز بالا میره.
این روزها ممکنه یک نفر یک هفته صرف Refactor چیزی کنه که:
هیچ Bugی نداشت.
هیچ Performance Problemی نداشت.
کسی از پیچیدگیش شکایت نداشت.
و هیچ نیازی هم قرار نبود در آینده حل کنه.
فقط چون:
«الان AI می‌تونه سریع انجامش بده.»
ولی سریع انجام شدن، به معنی ارزشمند بودن نیست.
اتفاقاً فکر می‌کنم هرچقدر AI بهتر بشه، نقش Engineering Judgment مهم‌تر می‌شه.
چون احتمالاً بخش زیادی از «چطور انجام بدیم؟» رو ابزارها جواب میدن.
اما هنوز یک سؤال باقی میمونه:
«اصلاً چرا داریم انجامش میدیم؟»
و شاید این همون جایی باشه که تفاوت یک Developer خوب با کسی که فقط میتونه از ابزارها استفاده کنه، بیشتر خودش رو نشون بده. AI می‌تونه بهت کمک کنه یک Refactor بزرگ رو در یک ساعت انجام بدی.
اما هنوز باید یک نفر تصمیم بگیره که:
آیا این یک ساعت، بهترین جایی بود که میشد خرجش کرد یا نه؟
هرچقدر ساختن و تغییر دادن ارزان‌تر میشه،
به نظرم توانایی «انجام ندادن کار اشتباه» ارزشمندتر میشه.
شاید در آینده، مهارت مهم مهندس‌ها این نباشه که چقدر سریع کد میزنن.
بلکه این باشه که بین هزار کاری که AI میتونه انجام بده،
تشخیص بدن کدومش واقعاً ارزش انجام دادن داره.
🎯 ءCache فقط Redis گذاشتن جلوی Database نیست!

یکی از سؤال‌های جالبی که در مصاحبه‌های Software Engineering ممکنه ازت بپرسن اینه:
«چه استراتژی‌هایی برای Caching می‌شناسی؟»
خیلی‌ها سریع جواب میدن:
«Cache Aside.»
و تمام.
اما وقتی وارد سیستم‌های واقعی می‌شیم، می‌بینیم که چند مدل مختلف برای مدیریت ارتباط بین Application، Cache و Database داریم.
بریم سراغ مهم‌ترین‌ها 👇

1️⃣ Cache-Aside / Lazy Loading

احتمالاً رایج‌ترین الگویی که در پروژه‌ها می‌بینید. Application ابتدا Cache را بررسی می‌کند.
مثلاً:
GET Product:123

اگر محصول داخل Redis باشد، همان را برمی‌گردانیم.
اگر نباشد:
Redis → Miss
↓
SQL Server
↓
Redis.Set(Product:123)
↓
Response

مزیت اصلی:
فقط چیزی را Cache می‌کنیم که واقعاً درخواست شده است.
برای Read-heavy workloadها و داده‌هایی مثل Product، User Profile و Catalog معمولاً انتخاب بسیار خوبی است.
📌اما یک نکته مهم دارد:
ءCache-Aside خودش Consistency بین Cache و Database را تضمین نمی‌کند.
معمولاً هنگام Update، Database را تغییر می‌دهیم و Entry مربوطه را Invalidate می‌کنیم تا درخواست بعدی دوباره آن را Load کند.

2️⃣ Read-Through

از بیرون شبیه Cache-Aside است، اما یک تفاوت مهم دارد.
در Cache-Aside:
ءApplication مسئول Fetch کردن از Database است.
در Read-Through:
ءCache Layer مسئول Fetch کردن Data Source است.
یعنی Application فقط با Cache کار می‌کند:
Application
↓
Cache
↓
Miss
↓
Data Store

این مدل می‌تواند Application Code را ساده‌تر کند، اما نیازمند Cache یا Infrastructureای است که بتواند این Fetch Logic را مدیریت کند.

3️⃣ Write-Through

اینجا داستان از سمت Write شروع می‌شود.
وقتی Data تغییر می‌کند، Cache و Primary Store باید در همان مسیر Write به‌روز شوند.
هدف این است که بعد از یک Write موفق، Cache هم نسخه به‌روز Data را داشته باشد.
مزیت؟
احتمال Cache Miss بعد از Write کمتر می‌شود و Readهای بعدی می‌توانند سریع باشند.
اما یک مشکل دارد:
ممکن است Dataای را وارد Cache کنیم که اصلاً کسی قرار نیست آن را بخواند.
در نتیجه:
ءMemory و Cache Churn بیشتری داریم.
برای همین Write-Through معمولاً به‌تنهایی استفاده نمی‌شود و می‌تواند در کنار Lazy/Cache-Aside قرار بگیرد.

4️⃣ Write-Behind / Write-Back

اینجا قضیه جدی‌تر می‌شود. Application ابتدا Data را در Cache می‌نویسد و Database در ادامه و به‌صورت Asynchronous به‌روزرسانی می‌شود.
Application
↓
Redis
↓
Async Worker
↓
Database

مثلاً یک سیستم با حجم بسیار زیاد:
Like Counter
View Counter
Analytics Event

ممکن است نخواهد برای هر Write منتظر Database بماند.
پس:
100,000 Writes
↓
Redis
↓
Batch
↓
Database

این مدل می‌تواند Write Throughput را بالا ببرد و فشار روی Database را کاهش دهد.
اما یک Trade-off بسیار مهم دارد: Durability.
اگر Data هنوز به Database نرسیده باشد و Cache از دست برود، ممکن است Data هم از دست برود.
بنابراین برای Dataهایی که از دست رفتنشان قابل قبول نیست، باید بسیار محتاط بود.

5️⃣ Write-Around

یک Pattern کمتر دیده‌شده: Write مستقیماً به Database می‌رود و Cache درگیر Write نمی‌شود.
Write
↓
Database

Read
↓
Cache
↓
Miss
↓
Database
↓
Cache

برای داده‌هایی که زیاد Write می‌شوند ولی بلافاصله Read نمی‌شوند، می‌تواند مفید باشد.
چون مجبور نیستیم هر Dataای که نوشته می‌شود را وارد Cache کنیم.