بهتر است Pipeline پردازش تصویر داشته باشیم:
در این معماری، API اصلی نباید مجبور باشد تمام پردازش تصویر را در همان Request انجام دهد.
برای تصاویر بزرگ یا پردازشهای سنگین میتوان:
را پیادهسازی کرد.
این طراحی باعث میشود Upload کاربر سریعتر پاسخ داده شود و پردازش سنگین تصویر به Worker منتقل شود. ⚙️
ءImageMagick ابزار قدرتمندی است؛ اما هر پردازش تصویری را نباید داخل Web Request انجام دهید.
فرض کنید کاربر یک تصویر بسیار بزرگ Upload کرده است.
اگر در همان Request:
را انجام دهید، با افزایش همزمانی ممکن است CPU و Memory سرور بهشدت مصرف شوند.
به همین دلیل در سیستمهای پرترافیک بهتر است:
داشته باشیم.
هر فایل تصویری که کاربر Upload میکند، الزاماً قابل اعتماد نیست.
نباید صرفاً به این اعتماد کنیم:
یا:
در یک سیستم واقعی باید Upload Validation، محدودیت اندازه فایل، محدودیت ابعاد تصویر و سیاستهای مناسب برای فرمتهای مجاز داشته باشیم.
همچنین ImageMagick را نباید با تنظیمات ناامن و بدون توجه به سیاستهای امنیتی محیط Production اجرا کرد.
یعنی:
⚖️ ءImageMagick را چه زمانی استفاده کنیم؟ ImageMagick انتخاب بسیار خوبی است وقتی که:
✅ فرمتهای تصویری متنوع دارید.
✅ عملیات پیچیده Image Processing دارید.
✅ به Resize، Crop، Composite، Conversion و Compression نیاز دارید.
✅ سیستم شما باید تعداد زیادی تصویر را پردازش کند.
✅ نیاز دارید یک Processing Pipeline مستقل برای تصاویر داشته باشید.
✅ میخواهید پردازش تصویر را به Worker منتقل کنید.
❌ چه زمانی شاید انتخاب مناسبی نباشد؟
اگر فقط میخواهید:
استفاده از یک کتابخانه سنگین ممکن است بیش از نیاز شما باشد.
همچنین اگر Processing شما کاملاً وابسته به قابلیتهای خاص GPU یا یک Pipeline تخصصی Computer Vision باشد، ImageMagick الزاماً بهترین ابزار نیست.
برای بعضی سناریوها کتابخانههای تخصصیتر انتخاب بهتری هستند.
🧠 یک اشتباه معماری رایج
این کد:
ممکن است در یک پروژه کوچک کاملاً قابل قبول باشد.
اما اگر همین API:
دریافت کند، داستان کاملاً متفاوت میشود.
پس سؤال اصلی این نیست که:
سؤال معماری مهمتر این است:
ءImageMagick فقط یک ابزار برای تبدیل
میتواند بخشی از یک Image Processing Pipeline باشد.
برای مثال:
و اینجاست که استفاده از آن در یک پروژه NET. واقعاً معنا پیدا میکند.
اگر فقط یک Resize ساده دارید، ساده نگهش دارید.
اما اگر با یک سیستم واقعی و پرتعداد از تصاویر سروکار دارید، ImageMagick + Worker + Object Storage + CDN میتواند یک ترکیب بسیار قدرتمند برای ساخت یک Image Processing 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، خوب یا بد نیستند.
هر کدام، هزینهها و مزایای خودشان را دارند.
و تصمیم درست،
همان تصمیمی است که با نیاز واقعی سیستم همخوانی داشته باشد.
یکی از تصمیمهایی که تقریباً هر تیمی دیر یا زود با آن روبهرو میشود این است:
«آیا این عملیات باید 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 نیاز داریم؟»
تصمیم خوب از این نمیآید که همه سریع بگویند:
«موافقم.»
گاهی از جایی شروع میشود که یک نفر جرئت میکند بگوید:
«صبر کنید، فکر کنم داریم یک چیزی رو نمیبینیم.»
در یک جلسه، تیم داشت درباره استفاده از یک تکنولوژی جدید تصمیم میگرفت. 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️⃣ از
غلط❌
درست✅
2️⃣ از .Wait() استفاده نکنید
غلط❌
درست✅
3️⃣ ءAsync All the Way را رعایت کنید
اگر یک متد Async است:
اجازه دهید Task در تمام مسیر حرکت کند.
نه اینکه وسط مسیر ناگهان:
یا:
صدا زده شود.
4️⃣ در Libraryها به Context وابسته نشوید
اگر Library شما نیازی به Context اصلی ندارد، میتوانید در نقاط مناسب از:
استفاده کنید.
5️⃣ از async void فقط برای Event Handler استفاده کنید
تقریباً در تمام Serviceها، Repositoryها، Handlerها و سایر متدهای Async:
یا:
انتخاب مناسبتری است.
ءDeadlock در دنیای Async معمولاً از خود async/await شروع نمیشود.
مشکل زمانی ایجاد میشود که:
یک عملیات Async را مجبور کنیم بهصورت Sync اجرا شود.
یعنی:
یا:
در محیطی که Continuation نیاز دارد دوباره روی یک Context بلاکشده اجرا شود.
راهکار اصلی؟
🚀 Async All the Way
و در صورت نیاز:
🔧 ConfigureAwait(false)
و مهمتر از همه:
در ASP.NET Core شاید Deadlock کلاسیک SynchronizationContext را کمتر ببینید، اما Sync-over-Async همچنان میتواند Thread Pool را اشباع کند و سیستم شما را زیر Load به زانو دربیاورد.
اگر میخواهید احتمال 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. ✅
ولی چند وقتیه بیشتر به این فکر میکنم که:
چند بار شده یک 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 کردنش پشیمون نشه.
یه چیزی که کمکم داره توی کارم تغییر میکنه، تعریفم از «تموم شدن کاره».
قبلاً وقتی یک 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 میتونه انجام بده،
تشخیص بدن کدومش واقعاً ارزش انجام دادن داره.
یه چیزی که این روزها بیشتر از خود 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 کنیم.
اما داستان اینجا تمام نمیشود.
حتی اگر بهترین Caching Pattern را انتخاب کنی، هنوز چند سؤال مهم باقی میماند:
ءCache چه زمانی Expire شود؟
چه زمانی Invalidate شود؟
اگر Redis Down شد چه کنیم؟
اگر یک Key ناگهان میلیونها Request داشت چه؟
اگر TTL تمام شد و هزار Request همزمان Cache Miss شدند چه؟
اینجاست که مفاهیمی مثل:
TTL
Cache Invalidation
Cache Stampede
Cache Penetration
Cache Breakdown
Cache Warming
Distributed Lock
Stale-While-Revalidate
وارد بازی میشوند.
پس بالاخره کدام Pattern را انتخاب کنیم؟
جواب حرفهای این نیست که:
«Cache-Aside بهترینه.»
یا:
«Write-Through بهتره.»
جواب این است:
بستگی به Workload و Consistency Requirement دارد.
مثلاً:
ءRead-heavy + تغییرات کم
→ Cache-Aside
ءRead-heavy + نیاز به Cache تازه بعد از Write
→ Cache-Aside + Write-Through
ءRead/Write بسیار سنگین + قابل قبول بودن Async Persistence
→ Write-Behind
ءWrite زیاد + Read کم
→ Write-Around
و در سیستمهای واقعی حتی ممکن است چند Pattern را همزمان داشته باشی.
در نهایت:
ءCaching یک تکنولوژی نیست.
یک Design Decision است.
و Redis فقط ابزاری است که این تصمیم را پیاده میکند.
حتی اگر بهترین Caching Pattern را انتخاب کنی، هنوز چند سؤال مهم باقی میماند:
ءCache چه زمانی Expire شود؟
چه زمانی Invalidate شود؟
اگر Redis Down شد چه کنیم؟
اگر یک Key ناگهان میلیونها Request داشت چه؟
اگر TTL تمام شد و هزار Request همزمان Cache Miss شدند چه؟
اینجاست که مفاهیمی مثل:
TTL
Cache Invalidation
Cache Stampede
Cache Penetration
Cache Breakdown
Cache Warming
Distributed Lock
Stale-While-Revalidate
وارد بازی میشوند.
پس بالاخره کدام Pattern را انتخاب کنیم؟
جواب حرفهای این نیست که:
«Cache-Aside بهترینه.»
یا:
«Write-Through بهتره.»
جواب این است:
بستگی به Workload و Consistency Requirement دارد.
مثلاً:
ءRead-heavy + تغییرات کم
→ Cache-Aside
ءRead-heavy + نیاز به Cache تازه بعد از Write
→ Cache-Aside + Write-Through
ءRead/Write بسیار سنگین + قابل قبول بودن Async Persistence
→ Write-Behind
ءWrite زیاد + Read کم
→ Write-Around
و در سیستمهای واقعی حتی ممکن است چند Pattern را همزمان داشته باشی.
در نهایت:
ءCaching یک تکنولوژی نیست.
یک Design Decision است.
و Redis فقط ابزاری است که این تصمیم را پیاده میکند.
🔖هشتگها:
#Redis #Caching #SoftwareEngineering #SystemDesign
🔐 امضای دیجیتال؛ وقتی یک قابلیت فنی میتواند به ریسک محصول تبدیل شود
در جریان پیادهسازی API مربوط به Digital Signature یکی از سرویسدهندهها، به نکتهای برخوردم که واقعاً برایم عجیب و البته نگرانکننده بود.
سرویس، Digital Certificate را برای کاربر صادر میکرد و فرآیند دریافت امضا هم انجام میشد؛ اما یک ارتباط مهم در این فرآیند وجود نداشت:
❌ ارتباط مشخص بین Certificate / Signature و Documentای که قرار است امضا شود.
یعنی در فرآیند دریافت امضا، عملاً مشخص نمیکردیم:
«این امضا دقیقاً برای کدام سند صادر میشود؟»
و این سؤال ساده، از دید Security و حتی Product بسیار مهم است.
🧩 مسئله دقیقاً چیست؟
فرض کنید کاربر برای امضای یک قرارداد خاص وارد سیستم شده است.
مثلاً: 📄 Contract #1024
کاربر تصور میکند قرار است همین قرارداد را امضا کند.
اما اگر فرآیند صدور امضا هیچ ارتباطی با Document نداشته باشد، ممکن است همان Certificate یا Signature در شرایط دیگری برای یک Document متفاوت مورد استفاده قرار بگیرد.
در این حالت سیستم ممکن است از نظر فنی بگوید:
✅ ءCertificate معتبر است
✅ ءSignature معتبر است
✅ امضا متعلق به این User است
اما یک سؤال مهم همچنان بیپاسخ میماند:
❓ آیا این کاربر واقعاً همین Document را برای امضا تأیید کرده است؟
و این دو مفهوم کاملاً متفاوت هستند.
⚠️ مشکل فقط Technical نیست
اینجا دقیقاً همان جایی است که باید از زاویه Product + Security به سیستم نگاه کنیم.
فرض کنید در یک سیستم مالی، دولتی یا حقوقی، کاربر به تصور اینکه دارد سند A را امضا میکند، فرآیند را تأیید میکند.
اما سیستم در لایه دیگری امکان استفاده از همان اعتبار برای سند B را فراهم میکند.
حتی اگر تمام APIها درست کار کنند و هیچ Exception یا خطای فنی وجود نداشته باشد، رفتار محصول میتواند مشکلدار باشد.
چون: Technical Validity ≠ User Intent
اینکه یک Signature از نظر Cryptography معتبر باشد، الزاماً به این معنی نیست که کاربر قصد داشته همان عملیات خاص را تأیید کند.
🔍 اینجا باید یک سؤال مهمتر بپرسیم
وقتی کاربر روی
Sign کلیک میکند، دقیقاً چه چیزی را تأیید کرده است؟آیا فقط هویت خودش را تأیید کرده؟
یا مشخصاً تأیید کرده که:
«من، User X، این Document مشخص با شناسه Y و نسخه Z را در این لحظه برای امضا تأیید میکنم.»
این تفاوت کوچک در طراحی، میتواند از نظر امنیتی بسیار مهم باشد.
🛡 راهکاری که در طراحی خودمان در نظر گرفتیم
برای اینکه حداقل در سمت خودمان، User Consent و آگاهی کاربر نسبت به سندی که قرار است امضا کند واضحتر باشد، یک مرحله OTP به فرآیند اضافه کردیم. Flow به این شکل شد:
User
│
▼
Select Document
│
▼
Show Document Details
│
▼
Request Signature
│
▼
Generate OTP
│
▼
Send OTP to User
│
▼
Verify OTP
│
▼
Create Signature Request
│
▼
Sign Specific Document
اینجا OTP صرفاً یک Authentication Factor نیست.
در طراحی محصول ما، بخشی از فرآیند Explicit User Consent هم محسوب میشود.
یعنی قبل از اینکه عملیات حساس انجام شود، کاربر باید دوباره در همان Flow تأیید کند که قصد ادامه فرآیند را دارد.
🔗 اما فقط OTP کافی نیست
یک نکته مهم این است که اضافه کردن OTP بهتنهایی نباید به ما حس امنیت کاذب بدهد.
اگر بخواهیم این سیستم را اصولیتر طراحی کنیم، باید Signature Request را به یک Document مشخص Bind کنیم.
مثلاً:
SignatureRequest
----------------
Id
UserId
DocumentId
DocumentVersion
DocumentHash
CreatedAt
ExpiresAt
Status
در این حالت بهجای اینکه صرفاً بگوییم:
User X signed something
میتوانیم بگوییم:
User X
approved
Document Y
Version Z
with Hash H
و این دقیقاً همان چیزی است که در یک سیستم حساس ارزش دارد:
🎯 Binding the user's intent to a specific artifact
🔐 حتی Document Hash هم میتواند مهم باشد
فرض کنید کاربر Document را در لحظه
T1 مشاهده کرده است.بعد از آن، Document تغییر میکند.
اگر Signature فقط به
DocumentId وابسته باشد، ممکن است این سؤال ایجاد شود:کاربر دقیقاً کدام نسخه از Document را امضا کرده است؟
به همین دلیل در سیستمهای حساس، نگهداشتن چیزی مثل:
DocumentId
DocumentVersion
DocumentHash
میتواند بسیار مهم باشد.
در این صورت میتوانیم مشخص کنیم Signature مربوط به همان محتوایی بوده که کاربر تأیید کرده است، نه صرفاً یک رکورد با یک شناسه.
👀 یک نکته مهم برای Developerها
گاهی ما بهعنوان Developer فقط این سؤال را میپرسیم:
اما در سیستمهای حساس باید چند سؤال دیگر هم بپرسیم:
🔹 آیا User دقیقاً میداند چه چیزی را تأیید میکند؟
🔹 آیا عملیات به یک Business Object مشخص متصل است؟
🔹 اگر داده بعداً تغییر کرد، Signature همچنان چه چیزی را نمایندگی میکند؟
🔹 آیا میتوان یک Authorization را در Context دیگری استفاده کرد؟
🔹 آیا Audit Trail کافی برای اثبات Intent کاربر داریم؟
🔹 آیا سیستم فقط Valid بودن Signature را بررسی میکند یا Context امضا را هم بررسی میکند؟
امضای دیجیتال فقط این نیست که:
سؤال مهمتر این است:
برای همین، در طراحی سرویسهای حساس نباید فقط به Implementation نگاه کنیم.
گاهی لازم است از کد فاصله بگیریم و از خودمان بپرسیم:
اگر من جای کاربر بودم، آیا دقیقاً میدانستم چه چیزی را دارم تأیید میکنم؟
چون در نهایت، Security فقط جلوگیری از Attack نیست.
گاهی Security یعنی مطمئن شویم سیستم دقیقاً همان چیزی را انجام میدهد که کاربر فکر میکند دارد انجام میدهد. 🔐
گاهی ما بهعنوان Developer فقط این سؤال را میپرسیم:
«آیا API درست کار میکند؟»
اما در سیستمهای حساس باید چند سؤال دیگر هم بپرسیم:
🔹 آیا User دقیقاً میداند چه چیزی را تأیید میکند؟
🔹 آیا عملیات به یک Business Object مشخص متصل است؟
🔹 اگر داده بعداً تغییر کرد، Signature همچنان چه چیزی را نمایندگی میکند؟
🔹 آیا میتوان یک Authorization را در Context دیگری استفاده کرد؟
🔹 آیا Audit Trail کافی برای اثبات Intent کاربر داریم؟
🔹 آیا سیستم فقط Valid بودن Signature را بررسی میکند یا Context امضا را هم بررسی میکند؟
🎯 در نهایت
امضای دیجیتال فقط این نیست که:
«این Signature معتبر است؟»
سؤال مهمتر این است:
«این Signature دقیقاً چه چیزی را، توسط چه کسی، در چه زمانی و با چه میزان آگاهی و رضایتی تأیید کرده است؟»
برای همین، در طراحی سرویسهای حساس نباید فقط به Implementation نگاه کنیم.
گاهی لازم است از کد فاصله بگیریم و از خودمان بپرسیم:
اگر من جای کاربر بودم، آیا دقیقاً میدانستم چه چیزی را دارم تأیید میکنم؟
چون در نهایت، Security فقط جلوگیری از Attack نیست.
گاهی Security یعنی مطمئن شویم سیستم دقیقاً همان چیزی را انجام میدهد که کاربر فکر میکند دارد انجام میدهد. 🔐
#تصمیمهای_مهندسی | Engineering Decisions
یه Feature قرار بود دو هفتهای آماده بشه.
روز اول دربارهی Architecture بحث کردیم.
روز دوم دربارهی Database.
روز سوم دربارهی اینکه Sync باشه یا Async.
روز چهارم هنوز داشتیم گزینهها رو مقایسه میکردیم.
روز پنجم یکی گفت:
«بذاریم فعلاً بیشتر بررسی کنیم.»
هفته دوم هم گذشت. Feature هنوز نوشته نشده بود.
جالب اینجاست که هیچکس تصمیم اشتباهی نگرفته بود.
اصلاً تصمیمی نگرفته بودیم.
این یکی از چیزهایی بود که بعداً فهمیدم:
گاهی ترس از گرفتن تصمیم اشتباه،
باعث میشه تیم وارد چرخهی Analysis Paralysis بشه.
برای هر تصمیم:
و همیشه یک دلیل جدید برای ادامهی بررسی وجود دارد.
در حالی که خیلی از تصمیمهای Engineering برگشتپذیرند.
اگر امروز یک Implementation انتخاب کنیم و فردا بفهمیم مناسب نیست،میتوانیم تغییرش بدهیم.
اما اگر سه هفته فقط دربارهاش صحبت کنیم،
آن سه هفته دیگر برنمیگردد.
برای همین الان وقتی با یک تصمیم مواجه میشوم، اول میپرسم:
این تصمیم Reversible است یا Irreversible؟
اگر Reversible باشد، لازم نیست برای رسیدن به ۱۰۰٪ اطمینان صبر کنیم.
با اطلاعات کافی تصمیم میگیریم،
پیاده میکنیم،Measure میکنیم،
و اگر لازم بود تغییر میدهیم.
چون در Engineering، تصمیم بد قابل اصلاح است.
اما تصمیم نگرفتن هم هزینه دارد.
و گاهی خیلی بیشتر از چیزی که فکر میکنیم. Perfect Decision نداریم؛ فقط Decisionی داریم که با اطلاعات فعلی، منطقیترین گزینه است.
یه Feature قرار بود دو هفتهای آماده بشه.
روز اول دربارهی Architecture بحث کردیم.
روز دوم دربارهی Database.
روز سوم دربارهی اینکه Sync باشه یا Async.
روز چهارم هنوز داشتیم گزینهها رو مقایسه میکردیم.
روز پنجم یکی گفت:
«بذاریم فعلاً بیشتر بررسی کنیم.»
هفته دوم هم گذشت. Feature هنوز نوشته نشده بود.
جالب اینجاست که هیچکس تصمیم اشتباهی نگرفته بود.
اصلاً تصمیمی نگرفته بودیم.
این یکی از چیزهایی بود که بعداً فهمیدم:
گاهی ترس از گرفتن تصمیم اشتباه،
باعث میشه تیم وارد چرخهی Analysis Paralysis بشه.
برای هر تصمیم:
Option A
Option B
Option C
Option D
...
و همیشه یک دلیل جدید برای ادامهی بررسی وجود دارد.
در حالی که خیلی از تصمیمهای Engineering برگشتپذیرند.
اگر امروز یک Implementation انتخاب کنیم و فردا بفهمیم مناسب نیست،میتوانیم تغییرش بدهیم.
اما اگر سه هفته فقط دربارهاش صحبت کنیم،
آن سه هفته دیگر برنمیگردد.
برای همین الان وقتی با یک تصمیم مواجه میشوم، اول میپرسم:
این تصمیم Reversible است یا Irreversible؟
اگر Reversible باشد، لازم نیست برای رسیدن به ۱۰۰٪ اطمینان صبر کنیم.
با اطلاعات کافی تصمیم میگیریم،
پیاده میکنیم،Measure میکنیم،
و اگر لازم بود تغییر میدهیم.
چون در Engineering، تصمیم بد قابل اصلاح است.
اما تصمیم نگرفتن هم هزینه دارد.
و گاهی خیلی بیشتر از چیزی که فکر میکنیم. Perfect Decision نداریم؛ فقط Decisionی داریم که با اطلاعات فعلی، منطقیترین گزینه است.
🏗 راز معماری نابودنشدنی با Coupling و Cohesion
اگر بخواهیم فقط با دو مفهوم بفهمیم یک طراحی نرمافزاری چقدر سالم است، یکی از بهترین نقاط شروع این دو مفهوماند:
🔗 Coupling
🧩 Cohesion
خیلی وقتها این دو را کنار هم میشنویم، اما دقیقاً نمیدانیم چه مشکلی را حل میکنند.
بیایید از یک سؤال ساده شروع کنیم:
اگر فردا بخواهیم یک قسمت از سیستم را تغییر بدهیم، چند قسمت دیگر مجبور میشوند همراه آن تغییر کنند؟
پاسخ این سؤال، ما را مستقیماً به سمت Coupling میبرد.
🔗 ءCoupling چیست؟
ءCoupling یعنی میزان وابستگی بین Components مختلف سیستم.
هرچه دو Component برای کار کردن بیشتر به جزئیات یکدیگر وابسته باشند، Coupling آنها بیشتر است.
مثلاً:
{
اینجا
private readonly PaymentService _payment;
private readonly InventoryService _inventory;
private readonly EmailService _email;
public async Task CreateOrder()
{
await _payment.Pay();
await _inventory.Reserve();
await _email.Send();
}
}
OrderService مستقیماً به سه Component دیگر وابسته است.حالا فرض کنید API مربوط به
PaymentService تغییر کند.احتمالاً باید
OrderService را هم تغییر بدهیم.این یعنی یک تغییر محلی، اثر خود را به بیرون منتقل کرده است.
این همان چیزی است که در معماری میخواهیم تا حد امکان کنترلش کنیم.
🧩 ءCohesion چیست؟
ءCohesion تقریباً سؤال برعکس را میپرسد:
چیزهایی که داخل یک Component قرار دادهایم، واقعاً چقدر به یکدیگر مربوط هستند؟
اگر یک کلاس، Module یا Service روی یک هدف مشخص متمرکز باشد، Cohesion بالاتری دارد.
مثلاً:
├── AddItem()
├── RemoveItem()
├── CalculateTotal()
└── ApplyDiscount()
تمام این رفتارها حول یک مفهوم مشترک قرار گرفتهاند:🎯 Order
این یعنی Cohesion مناسب.
اما اگر همان کلاس تبدیل شود به:
├── CreateOrder()
├── SendEmail()
├── ResizeImage()
├── GenerateReport()
├── CreateUser()
├── ProcessPayment()
└── ClearCache()
دیگر یک مسئولیت مشخص نداریم.
کلاس به یک محل تجمع Business Logicهای نامرتبط تبدیل شده است.
اینجا Cohesion پایین آمده است.
🎯 پس هدف معماری چیست؟
یک قاعده بسیار مهم:
High Cohesion + Low Coupling
یعنی:
🧩 داخل هر Component، چیزهایی که واقعاً به هم مربوطاند کنار هم باشند.
و:
🔗 بین Componentها، وابستگی غیرضروری حداقل باشد.
این ترکیب یکی از اصول مهم در طراحی سیستمهای قابل نگهداری و قابل تغییر است. AWS نیز در راهنمای معماری خود برای Microservices صراحتاً بر Loose Coupling و High Functional Cohesion تأکید میکند.
💣 اما یک نکته مهم وجود دارد... Low Coupling به معنی:
«هیچ وابستگیای نباید وجود داشته باشد»
نیست.
این تصور اشتباه است.
یک سیستم بدون وابستگی عملاً وجود ندارد.
مثلاً:
↓
Payment
ءOrderبرای انجام یک فرآیند ممکن است واقعاً به Payment نیاز داشته باشد.
مسئله این نیست که Dependency را صفر کنیم.
مسئله این است که:
ءDependencyها را در مرزهای درست قرار دهیم.
🎯 یک معیار بسیار کاربردی
وقتی میخواهی تصمیم بگیری دو Component باید کنار هم باشند یا جدا، این سؤالها را بپرس:
🔹 آیا معمولاً با هم تغییر میکنند؟
🔹 آیا برای انجام یک Business Capability به یکدیگر نیاز دارند؟
🔹 آیا دادههایشان به شدت به هم وابسته است؟
🔹 آیا Transactionهای مشترک دارند؟
🔹 آیا یکی بدون دیگری میتواند مستقل Deploy شود؟
🔹 آیا یکی باید بتواند بدون دیگری کار کند؟
🔹 آیا تیمهای متفاوت مسئول آنها هستند؟
این سؤالها به ما کمک میکنند Boundary را بر اساس رفتار واقعی سیستم پیدا کنیم، نه بر اساس سلیقه.
Forwarded from کدهالیک | codehalic
وقتی بکاند پروژه C# باشه و فرانتاند TypeScript، بزرگترین دردسر اینه که مجبوری ساختار دیتا (مثل DTOها) رو دو جا تعریف کنی و اگه فیلدی تو بکاند عوض بشه، تازه تو Runtime ارورها خودشون رو نشون میدن. اما با معماری Monorepo و ابزار Nx میشه یه Type Safety یکپارچه و فولاستک ساخت! راهحلش اینه که به کمک OpenAPI موقع بیلد شدن پروژهی داتنت، کانترکتهای API به صورت اتوماتیک تولید بشن؛ بعد ابزاری مثل
https://nx.dev/blog/dotnet-openapi-type-safety
@codehalics | کدهالیک
openapi-ts این خروجی رو میخونه و تایپهای فرانتاند رو دقیقاً از روش جنریت میکنه. شاهکارِ Nx اینجاست که با سیستم قدرتمند Task Graph خودش، این پروسه رو به هم زنجیر و هوشمندانه کش Cache میکنه؛ یعنی به محض اینکه یه پراپرتی رو تو کدهای C# تغییر بدی، Nx خودش جریان رو میفهمه، تایپها رو آپدیت میکنه و خطای ناهماهنگی فرانتاند رو همون لحظه تو زمان کامپایل نشون میده تا دیگه هیچوقت باگهای ناشی از تغییر API به پروداکشن نرسن!https://nx.dev/blog/dotnet-openapi-type-safety
@codehalics | کدهالیک