درسهای فنی کلیدی 🧠💡
• معماری ماژولار بازده دارد:
رویکرد modular monolith باعث شد سیستم قابلدرکتر و قابلنگهداریتر باشد. هر ماژول محدوده و مسئولیتهای روشن داشت.
• سرمایهگذاری روی Deployment Automation:
وجود CI/CD pipeline حیاتی بود. امکان استقرارهای سریع و مطمئن را فراهم کرد و ریسک هر تغییر را کاهش داد. 🚀
• یکپارچهسازی مبتنی بر پیام:
ارتباط async میان ماژولها انعطافپذیری لازم برای مهاجرت تدریجی را فراهم کرد.
• پیچیدگی Data Sync:
هرگز پیچیدگی همگامسازی داده را در مهاجرتهای legacy دستکم نگیرید. چه ابزارهای موجود را استفاده کنید، چه راهکار custom بسازید، این بخش یکی از بزرگترین چالشهاست. ⚠️
عامل انسانی 👥
چالشهای فنی تنها بخشی از ماجرا هستند. موفقیت در بازنویسی سیستمهای legacy شدیداً وابسته به مدیریت ذینفعان است:
🔸️ءProduct Management باید progress ببیند
🔸️تیم توسعه نیاز دارد کار را درست انجام دهد
🔸️کسبوکار باید بدون توقف ادامه پیدا کند
🔸️تیم legacy باید انتقال دانش انجام دهد
🔸️یافتن تعادل میان این نیازهای متعارض، کار آسانی نبود.
ما از چند رویکرد مؤثر استفاده کردیم:
🔹️جلسات منظم ذینفعان برای بیان دغدغهها
🔹️ردیابی شفاف پروژه که برای همه قابل مشاهده بود
🔹️توضیح شفاف تصمیمات فنی و تأثیر آنها بر کسبوکار
🔹️جشنگرفتن milestoneهای فنی و تجاری 🎉
مستندسازی دانش فنی و دانش سازمانی 📚
اهمیت مستندسازی تجربهی ۴۰ سالهی تیم legacy را نمیتوانم دستکم بگیرم.
وقتی تیم اصلی بازنشسته شد، ما مجموعهی کاملی از اسناد داشتیم که تکتک business ruleها و edge caseها را توضیح میداد.
دستاوردهای واقعی 🌟📈
چهار سال بعد، سیستم در وضعیت بسیار خوبی قرار دارد.
زیرساخت cloud، قابلیت اطمینان و مقیاسپذیری را فراهم کرده است.
معماری modular monolith سیستم را قابلنگهداری کرده است.Pipelineهای خودکار امکان استقرار سریع را فراهم میکنند.
اما این مسیر به ما آموخت که باید میان فشارهای کسبوکاری و نیازهای فنی تعادل ایجاد کرد. موفقیت در مهاجرت سیستمهای legacy فقط به برتری فنی وابسته نیست — بلکه نیازمند درک حوزهی کسبوکار، مدیریت انتظارات ذینفعان، و اتخاذ تصمیمات معماری واقعگرایانه است.
معماری نرمافزار مهم است، اما عامل انسانی نیز به همان اندازه اهمیت دارد.
برای هر دو برنامهریزی کنید. 🤝🏗
متشکرم از اینکه خواندید.
و همیشه فوقالعاده بمانید! 🚀
اگر EF Core خودش Repository Pattern را ارائه میدهد، چرا باید یک abstraction دیگر روی آن بسازیم؟ 🤔📦یکی از بحثهای همیشگی در جامعهی .NET این است:
آیا هنوز باید از الگوهای Repository و Unit of Work استفاده کنیم، یا این الگوها در برنامههای مدرن منسوخ شدهاند یا حتی مضر هستند؟
بههرحال، اگر روی DbContext در EF Core hover کنید، مایکروسافت بهوضوح میگوید:
A DbContext instance represents a session with the database and is a combination of the Unit of Work and Repository patterns.
این یعنی EF Core همین حالا این الگوها را پیادهسازی میکند:
• ءDbContext نقش Unit of Work را بازی میکند
• ء<DbSet<TEntity نقش Repository را ایفا میکند
پس سؤال طبیعی این است:
وقتی EF Core همین حالا این abstractions را فراهم میکند، چرا باید یک abstraction دیگر روی آن بسازیم — یعنی یک «abstraction روی abstraction»؟ 🧱🧱
بسیاری از توسعهدهندگان همچنین معتقدند نوشتن repositoryهای سفارشی باعث میشود قابلیتهای قدرتمند EF Core از دید پنهان شود، مانند:
• Eager Loading
• AsNoTracking
• Projections
• Query Composition
• Global Query Filters
• Split Queries
بازنویسی همهی اینها داخل Repositoryهای custom معمولاً به یک نتیجه منتهی میشود:
«ما فقط داریم DbContext را دوباره اختراع میکنیم، فقط با قدمهای اضافی.» 🔁🙃
یک استدلال رایج دیگر testability است.
برخی میگویند برای تستهای واحد باید repository داشته باشیم تا بتوانیم DbContext را mock کنیم؛ اما EF Core همین حالا یک InMemory provider عالی دارد که تستها را واقعیتر میکند و نیاز به mock کردن کل لایهی persistence را از بین میبرد. 🧪🚫
و سپس یک ترس دیگر هم وجود دارد:
«شاید یک روز ORM را عوض کنیم.»
اما در ۹۹٪ پروژههای واقعی این اتفاق نمیافتد.
و حتی اگر هم بیفتد، تغییر فقط repository interfaceها را شامل نمیشود، بلکه بخشهای بسیار بیشتری از سیستم را لمس خواهد کرد — پس این استدلال عمدتاً یک نقض آشکارِ YAGNI است. ⚠️🧠
البته هر Repository اضافی که ایجاد میکنید، سربار نگهداری را افزایش میدهد:
کلاسهای بیشتر 📦
اینترفیسهای بیشتر 🧩
ساختارهای اتصال بیشتر 🔌
پیچیدگی بیشتر 🧠
پس… آیا Repository Pattern مضر است؟ یا ما آن را اشتباه استفاده میکنیم؟
واقعیت، ظریفتر از این حرفهاست.
بسیاری از نظرات منفی ناشی از سوءبرداشت از هدف Repository هستند.Repository هرگز قرار نبود تمام قابلیتهای persistence framework را در معرض دید قرار دهد.
هدف آن این نیست که API مربوط به DbContext را بازتولید کند.
وظیفهٔ اصلی آن این است که کنترل کند دامنه چگونه با داده تعامل میکند.
🎯 Repository واقعاً چه هدفی دارد؟
بهجای ارائهٔ یک سطح دسترسی باز و نامحدود به داده، Repository یک سری عملیات Aggregate-focused، صریح، و intention-revealing تعریف میکند.
این کار چند مزیت مهم به همراه دارد:
منطق کوئری قابلپیشبینی و قابل نگهداری
رفتار کوئری ثابت و در طول زمان بهینهتر میشود.
شفافیت برای domain expertها
نام متدها هدف و مفهوم تجاری را بیان میکنند؛ نیازی نیست SQL یا LINQ بلد باشند.
عملیات دامنهمحور بهجای CRUD عمومی
بهجای اینکه هر نوع دستکاری entity را در اختیار بگذارد، رفتارهایی ارائه میدهد که واقعاً برای دامنه معنادار است.
🛡 هدف واقعی: محافظت از Domain Model
ءRepository pattern در درجهٔ اول دربارهٔ اینها نیست:
• آسانتر کردن unit testing
• امکان تعویض Database
• بستهبندی امکانات EF Core
هدف اصلی آن این است که Domain Model را از نشت نگرانیهای persistence محافظت کند.
در غیاب یک مرز Repository، منطق persistence دیر یا زود وارد دامنه میشود، وضوح را کاهش میدهد، coupling را افزایش میدهد، و مدل را تضعیف میکند.
⛔️ چه زمانی نباید از Repository استفاده کنید؟
• زمانی که دامنهٔ شما ساده است
• زمانی که CRUD کافی است
• زمانی که میخواهید از قابلیتهای EF Core بهصورت مستقیم استفاده کنید
در این موارد، Repository فقط پیچیدگی غیرضروری اضافه میکند.
✅ و چه زمانی باید از Repository استفاده کنید؟
• وقتی با یک دامنهٔ پیچیده و غنی از رفتار سروکار دارید
در چنین سیستمهایی، Repository تبدیل به بخشی از دامنه میشود، قدرت بیان مدل را افزایش میدهد و مرزهای Aggregate را تقویت میکند.
✨ جمعبندی
ءRepository pattern ضدالگو نیست
استفادهٔ اشتباه از آن ضدالگوست.
وقتی آگاهانه و در دامنههای غنی و پیچیده بهکار گرفته شود، به ابزاری قدرتمند برای بیان intent تجاری و حفظ یک معماری تمیز و مقاوم تبدیل میشود.
ساده نگهش دارید.
📌Daniel Jajimi
🔖هشتگها:
#DDD #CleanArchitecture #RepositoryPattern #SoftwareArchitecture #AggregateDesign
معماری Vertical Slice: منطق مشترک دقیقاً کجا باید قرار بگیرد؟ 🚀
معماری Vertical Slice Architecture (VSA) وقتی برای اولین بار با آن روبهرو میشوید شبیه یک نسیم تازه است.
دیگر برای افزودن یک فیلد مجبور نیستید بین هفت لایه جابهجا شوید. پروژههای متعدد را از داخل Solution حذف میکنید. احساس آزادی میکنید.
اما وقتی شروع به پیادهسازی قابلیتهای پیچیدهتر میکنید، ترکها شروع به نمایان شدن میکنند. ⚠️
یک Slice برای CreateOrder میسازید. سپس UpdateOrder. بعد GetOrder.
ناگهان متوجه تکرارها میشوید:
• منطق اعتبارسنجی آدرس در سه مکان تکرار شده است.
• الگوریتم قیمتگذاری هم در Cart نیاز است و هم در Checkout.
• احساس میکنید باید یک Common project یا یک SharedServices folder بسازید.
این لحظه، بحرانیترین نقطه در مسیر پذیرش VSA است.
اگر اشتباه انتخاب کنید، همان coupling که قصد داشتید از آن فرار کنید را دوباره برمیگردانید. 🔄
اگر درست انتخاب کنید، استقلالی را حفظ میکنید که VSA را ارزشمند کرده است.
در ادامه توضیح میدهم که چطور من با shared code در Vertical Slice Architecture برخورد میکنم.
گاردریلها در برابر جادهٔ باز 🛣
برای اینکه بفهمیم چرا این موضوع سخت است، باید به چیزی که پشت سر گذاشتهایم نگاه کنیم.Clean Architecture گاردریلهای سختگیرانه ارائه میکند.
کاملاً مشخص میکند که هر کدی دقیقاً کجا زندگی میکند:
• Entities در Domain
• Interfaces در Application
• Implementations در Infrastructure
امن است. جلوی خطاها را میگیرد.
اما همچنین جلوی میانبرهای ضروری را نیز میگیرد.
در مقابل، Vertical Slice Architecture گاردریلها را حذف میکند.
این معماری میگوید:
"کد را بر اساس قابلیتها سازماندهی کن، نه بر اساس دغدغههای تکنیکی."
این کار سرعت و انعطافپذیری به شما میدهد،
اما بار انضباط معماری را بر دوش خودتان میگذارد.
پس چه باید کرد؟ 🤔
تله: کشوی آشفتگی به نام "Common" 🗃
سادهترین مسیر این است که یک پروژه یا فولدر به نامهای Shared, Common, یا Utils بسازید.
این کار تقریباً همیشه یک اشتباه است. ❌
فرض کنید پروژهای دارید به نام Common.Services همراه با یک کلاس OrderCalculationService.
این کلاس:
• یک متد برای جمع Cart دارد (مورد استفادهی Cart)
• یک متد برای درآمد تاریخی دارد (مورد استفادهی Reporting)
• یک Helper برای فرمتکردن فاکتور دارد (مورد استفادهی Invoices)
سه concern کاملاً بیربط.
سه نرخ تغییر متفاوت.
و یک کلاس که همهٔ اینها را به یکدیگر couple کرده است. 🕸
پروژهٔ Common دیر یا زود تبدیل میشود به یک junk drawer،
محلی برای هر چیزی که حوصلهٔ نامگذاری یا جایگذاری درستش را ندارید.
نتیجه؟
یک شبکهٔ پیچیده از وابستگیها که در آن قابلیتهای مستقل، فقط چون یک Helper مشترک استفاده میکنند، به هم گره میخورند.
در واقع coupling که قصد داشتید از آن فرار کنید دوباره بازمیگردد. 🔁
چارچوب تصمیمگیری 🧭
وقتی به یک موقعیت بالقوهٔ اشتراکگذاری (Sharing) میرسم، سه سؤال از خودم میپرسم:
1️⃣ آیا این موضوع Infrastructural است یا Domain؟
موارد Infrastructure مثل database contexts، logging، HTTP clients تقریباً همیشه باید Shared باشند.
اما مفاهیم Domain نیاز به بررسی دقیقتری دارند.
2️⃣ این کانسپت چقدر پایدار است؟
اگر سالی یک بار تغییر میکند → Shared کردن مناسب است.
اگر همراه با هر Feature Request تغییر میکند → محلی نگهش دارید (Local).
3️⃣ آیا از «Rule of Three» عبور کردهام؟
یک بار Duplicate کردن مشکلی ندارد.
دو بار هم قابل تحمل است.
اما سه بار تکرار باید برای شما زنگ خطر باشد.
تا قبل از رسیدن به سه، Abstraction انجام ندهید.
ما اینها را با Refactor کردن حل میکنیم. بیایید مثالها را ببینیم. 🔍
سه سطح اشتراکگذاری
بهجای اینکه فقط دو گزینهٔ «Shared» یا «Not Shared» داشته باشید، در سه سطح فکر کنید.
Tier 1: Technical Infrastructure (کاملاً قابل اشتراک) ⚙️
کدهای Plumbing که تمام Sliceها به یک اندازه از آن بهره میبرند:
• Logging adapters
• Database connection factories
• Auth middleware
•الگوی Result
•Validation pipelines
این موارد را در یک پروژهٔ Shared.Kernel یا Infrastructure قرار دهید.
همچنین میتوانند فقط یک فولدر باشند.
این بخشها بهندرت بهخاطر نیازهای کسبوکار تغییر میکنند.
نمونهٔ مناسب اشتراکگذاری: Technical Kernel
public readonly record struct Result
{
public bool IsSuccess { get; }
public string Error { get; }
private Result(bool isSuccess, string error)
{
IsSuccess = isSuccess;
Error = error;
}
public static Result Success() => new(true, string.Empty);
public static Result Failure(string error) => new(false, error);
}
Tier 2: Domain Concepts (اشتراکگذاری و انتقال منطق به پایینترین سطح) 🧩
این یکی از بهترین مکانها برای Shared کردن منطق است.
بهجای پخشکردن Business Ruleها در Sliceهای مختلف، منطق را در Entities و Value Objects قرار دهید.
نمونهٔ مناسب: Entity با منطق تجاری
public class Order
{
public Guid Id { get; private set; }
public OrderStatus Status { get; private set; }
public List<OrderLine> Lines { get; private set; }
public bool CanBeCancelled() => Status == OrderStatus.Pending;
public Result Cancel()
{
if (!CanBeCancelled())
{
return Result.Failure("Only pending orders can be cancelled.");
}
Status = OrderStatus.Cancelled;
return Result.Success();
}
}
حالا Sliceهای زیر همگی دقیقاً از همین قوانین استفاده میکنند:
CancelOrder
GetOrder
UpdateOrder
منطق فقط در یک مکان زندگی میکند.
این نکتهٔ مهم را نشان میدهد:
ءSliceهای مختلف میتوانند Domain Model یکسانی را به اشتراک بگذارند.
Tier 3: Feature-Specific Logic (محلی نگه دارید) 📌
منطق مشترک بین Sliceهای مرتبط — مانند CreateOrder و UpdateOrder — نیازی ندارد که Global شود.
میتوانید یک فولدر کوچک Shared در داخل یک Feature ایجاد کنید:
📂 Features
└──📂 Orders
├──📂 CreateOrder
├──📂 UpdateOrder
├──📂 GetOrder
└──📂 Shared
├──📄 OrderValidator.cs
└──📄 OrderPricingService.cs
این یک مزیت پنهان هم دارد:
اگر یک روز Feature مربوط به Orders را حذف کنید، Shared logic مخصوص آن هم حذف میشود.
هیچ کد مردهای (Zombie Code) باقی نمیماند. 🧟♂️❌
اشتراکگذاری بین Featureهای مختلف 🚦
در Vertical Slice Architecture، اشتراکگذاری کد بین Featureهای نامرتبط چطور انجام میشود؟
ءCreateOrder باید بررسی کند آیا Customer وجود دارد یا نه.GenerateInvoice باید Tax را محاسبه کند.Orders و Customers هر دو باید پیامهای Notification را فرمت کنند.
اینها در یک فولدر Shared مخصوص Feature جا نمیگیرند. پس کجا باید بروند؟
اول، بپرسید: آیا واقعاً نیاز به اشتراکگذاری وجود دارد؟
بیشتر اشتراکگذاریهای cross-feature در واقع Data Access در پوشش جدید هستند.
اگر CreateOrder به اطلاعات Customer نیاز دارد، باید مستقیماً دیتابیس را Query کند.
نباید Feature مربوط به Customers را صدا بزند.
هر Slice مالک دسترسی به دادههای خودش است.Entity مربوط به Customer Shared است (در Domain قرار دارد)،
اما Service اشتراکی بین آنها وجود ندارد.
وقتی واقعاً نیاز به اشتراکگذاری منطق دارید، بپرسید ماهیت آن چیست:
اگر Domain Logic است (Business Rules یا Calculations) → Domain/Services
اگر Infrastructure است (APIهای خارجی، Formatting) → Infrastructure/Services
نمونه:
// Domain/Services/TaxCalculator.cs
public class TaxCalculator
{
public decimal CalculateTax(Address address, decimal subtotal)
{
var rate = GetTaxRate(address.State, address.Country);
return subtotal * rate;
}
}
هم CreateOrder و هم GenerateInvoice میتوانند از آن استفاده کنند بدون اینکه به هم Coupled شوند.
قبل از ساخت هر Service برای اشتراکگذاری cross-feature، بپرسید:
آیا این منطق میتواند روی یک Domain Entity قرار بگیرد؟
بیشتر "shared business logic"ها در واقع:
• ءdata access هستند
• ءdomain logicی هستند که باید روی Entity قرار بگیرند
• ءabstractionهای زودهنگاماند
اگر نیاز دارید یک side effect در Feature دیگری ایجاد کنید، پیشنهاد میشود:
از Messaging و Event استفاده کنید
یا Feature مقصد یک Facade (یک API عمومی) برای این عملیات ارائه کند
زمانی که Duplication انتخاب درست است 🔁
گاهی چیزی Shared به نظر میرسد، اما واقعاً Shared نیست.
هر دو یکساناند.
وسوسهٔ ساخت یک SharedOrderDto شدید است.
مقاومت کنید.
هفتهٔ بعد، GetOrder نیاز به tracking URL پیدا میکند.
اما CreateOrder وقتی اجرا میشود هنوز Shipping انجام نشده؛ پس URL وجود ندارد.
یک property nullable اضافه میشد
نیمی از مواقع خالی بود
و باعث ابهام و Coupling میشد.Duplication ارزانتر از Abstraction اشتباه است.
این ساختاری است که یک پروژهٔ بالغ Vertical Slice Architecture معمولاً دارد:
توضیح بخشها:
🔸️ءFeatures → Sliceهای مستقل. هرکدام صاحب Request/Response خودشاناند.
🔸️ءFeatures/[Name]/Shared → اشتراکگذاری محلی بین Sliceهای مرتبط یک Feature.
🔸️ءDomain → Entities، Value Objects، Domain Services.
🔸️ءInfrastructure → کل نگرش فنی سیستم.
🔸️ءShared → فقط Cross-Cutting Behaviors.
بعد از ساخت چند سیستم با این معماری، به این اصول رسیدهام:
1️⃣ ءFeatures صاحب Request/Response خودشان هستند. بدون استثنا.
2️⃣ منطق تجاری را تا حد ممکن وارد Domain کنید.
ءEntities و ValueObjectها بهترین مکان برای اشتراکگذاری واقعی Business Rules هستند.
3️⃣ اشتراکگذاری در سطح Feature-Family را محلی نگه دارید.
فقط اگر کد فقط در Orderها استفاده میشود → همانجا نگه دارید.
4️⃣ ءInfrastructure بهصورت پیشفرض Shared است.
Persistence، Logging، HTTP Clients
5️⃣ ءRule of Three را رعایت کنید.
تا وقتی سه استفادهٔ واقعی و مشابه ندارید → abstraction نکنید.
ءVertical Slice Architecture از شما میپرسد:
«این کد متعلق به کدام Feature است؟»
سؤال اشتراکگذاری در واقع میپرسد:
«اگر جوابش چند Feature است چه کنم؟»
پاسخ:
پذیرفتن اینکه برخی مفاهیم واقعاً cross-feature هستند،
و دادن یک محل مشخص به آنها بر اساس ماهیتشان: Domain، Infrastructure یا Behavior.
هدف، حذف کامل Duplication نیست.
هدف این است که وقتی نیازها تغییر میکند، تغییر کد ارزان و ساده باشد.
و نیازها همیشه تغییر میکنند.
Thanks for reading.
And stay awesome! ✨
آیا این منطق میتواند روی یک Domain Entity قرار بگیرد؟
بیشتر "shared business logic"ها در واقع:
• ءdata access هستند
• ءdomain logicی هستند که باید روی Entity قرار بگیرند
• ءabstractionهای زودهنگاماند
اگر نیاز دارید یک side effect در Feature دیگری ایجاد کنید، پیشنهاد میشود:
از Messaging و Event استفاده کنید
یا Feature مقصد یک Facade (یک API عمومی) برای این عملیات ارائه کند
زمانی که Duplication انتخاب درست است 🔁
گاهی چیزی Shared به نظر میرسد، اما واقعاً Shared نیست.
// Features/Orders/GetOrder
public record GetOrderResponse(Guid Id, decimal Total, string Status);
// Features/Orders/CreateOrder
public record CreateOrderResponse(Guid Id, decimal Total, string Status);
هر دو یکساناند.
وسوسهٔ ساخت یک SharedOrderDto شدید است.
مقاومت کنید.
هفتهٔ بعد، GetOrder نیاز به tracking URL پیدا میکند.
اما CreateOrder وقتی اجرا میشود هنوز Shipping انجام نشده؛ پس URL وجود ندارد.
اگر DTO مشترک ساخته بودید:
یک property nullable اضافه میشد
نیمی از مواقع خالی بود
و باعث ابهام و Coupling میشد.Duplication ارزانتر از Abstraction اشتباه است.
ساختار عملی 🏗
این ساختاری است که یک پروژهٔ بالغ Vertical Slice Architecture معمولاً دارد:
📂 src
└──📂 Features
│ ├──📂 Orders
│ │ ├──📂 CreateOrder
│ │ ├──📂 UpdateOrder
│ │ └──📂 Shared # اشتراکگذاری مخصوص Orders
│ ├──📂 Customers
│ │ ├──📂 GetCustomer
│ │ └──📂 Shared # اشتراکگذاری مخصوص Customers
│ └──📂 Invoices
│ └──📂 GenerateInvoice
└──📂 Domain
│ ├──📂 Entities
│ ├──📂 ValueObjects
│ └──📂 Services # منطق domain مشترک
└──📂 Infrastructure
│ ├──📂 Persistence
│ └──📂 Services
└──📂 Shared
└──📂 Behaviors
توضیح بخشها:
🔸️ءFeatures → Sliceهای مستقل. هرکدام صاحب Request/Response خودشاناند.
🔸️ءFeatures/[Name]/Shared → اشتراکگذاری محلی بین Sliceهای مرتبط یک Feature.
🔸️ءDomain → Entities، Value Objects، Domain Services.
🔸️ءInfrastructure → کل نگرش فنی سیستم.
🔸️ءShared → فقط Cross-Cutting Behaviors.
قوانین 🧠
بعد از ساخت چند سیستم با این معماری، به این اصول رسیدهام:
1️⃣ ءFeatures صاحب Request/Response خودشان هستند. بدون استثنا.
2️⃣ منطق تجاری را تا حد ممکن وارد Domain کنید.
ءEntities و ValueObjectها بهترین مکان برای اشتراکگذاری واقعی Business Rules هستند.
3️⃣ اشتراکگذاری در سطح Feature-Family را محلی نگه دارید.
فقط اگر کد فقط در Orderها استفاده میشود → همانجا نگه دارید.
4️⃣ ءInfrastructure بهصورت پیشفرض Shared است.
Persistence، Logging، HTTP Clients
5️⃣ ءRule of Three را رعایت کنید.
تا وقتی سه استفادهٔ واقعی و مشابه ندارید → abstraction نکنید.
نتیجهگیری 📌
ءVertical Slice Architecture از شما میپرسد:
«این کد متعلق به کدام Feature است؟»
سؤال اشتراکگذاری در واقع میپرسد:
«اگر جوابش چند Feature است چه کنم؟»
پاسخ:
پذیرفتن اینکه برخی مفاهیم واقعاً cross-feature هستند،
و دادن یک محل مشخص به آنها بر اساس ماهیتشان: Domain، Infrastructure یا Behavior.
هدف، حذف کامل Duplication نیست.
هدف این است که وقتی نیازها تغییر میکند، تغییر کد ارزان و ساده باشد.
و نیازها همیشه تغییر میکنند.
Thanks for reading.
And stay awesome! ✨
🔖هشتگها:
#VerticalSliceArchitecture #SoftwareArchitecture #DotNet #CSharp #ArchitecturePatterns
تفاوت IEnumerable و IEnumerator
یادمه در یکی از مصاحبه هایی که داشتم مصاحبه کننده وارد مبحث Collection ها شد و ازم خواست تفاوت IEnumerable و IEnumerator رو توضیح بدم.
منم چون فقط اسمشونو شنیده بودم گفتم هردو اینترفیس های Collection هستن
و وقتی مصاحبه کننده گفت: خب؟! همین...؟؟!
تازه فهمیدم چقدر زیاد راجب این دو و تفاوت هاشون نمیدونم...
تفاوت IEnumerable و IEnumerator
برای اینکه موضوع برام جا بیفته، یک مثال واقعی کمکم کرد.
ببینیم IEnumerable چیه؟
فرض کن وارد یک کتابخانه میشیم.
اولین چیزی که می بینیم لیست موضوعات کتابهاست: رمان، فلسفه، تاریخی، علمی و…
اما هنوز به هیچ کتابی نزدیک نشدیم، چیزی ورق نزدیم و هیچکدومو انتخاب نکردیم.
درواقع IEnumerable دقیقاً همین کار رو میکنه:
فقط میگه این مجموعه قابل پیمایشه و میشه روش iterat کرد.
نه آیتم میاره، نه جلو و عقب میره، نه حرکت میکنه.
و IEnumerator چیه؟
حالا نوبت کتابداره! IEnumerator همون کتابداریه که:
حرکت میکنه
آیتم فعلی رو میاره
و حتی دوباره میتونه به اول لیست برگرده
و سه قابلیت اصلی و کلیدی داره:
حرکت به آیتم بعدی : ()MoveNext
آیتم فعلی : Current
برگشت به ابتدا : ()void Reset
یعنی تنها «عامل حرکت» در تمامی collection ها ، IEnumerator هست.
و حالا یه اتفاق جالب رخ میده وقتی ما روی یک لیست حلقه میزنیم.
وقتی مینویسیم:
foreach (var item in list)
کامپایلر اینو تبدیل میکنه به:
IEnumerator enumerator=list.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
}
یعنی foreach دقیقا این کار رو انجام میده:
از IEnumerable → یک Enumerator جدید میگیره و باهاش پیمایش انجام میده.
خلاصه تفاوت های اصلی:
🔸️IEnumerable :
مشخص میکنه یک مجموعه قابل پیمایشه (مثل فهرست کتابخانه)
🔹️IEnumerator:
یک الگوریتم پیمایشه (مثل کتابداری که حرکت میکنه و کتاب میاره)
این موضوع شاید وقتیکه متوجه ش شدم ساده بهنظر رسید،
اما فهمیدن همین مفاهیم پایه و به ظاهر ساده ست که کیفیت کد و صاحب کد رو مشخص میکنه.
🔗Link
🚫 ءDbContext Thread-Safe نیست: اجرای موازی Queryهای EF Core به روش درست
ما همه یک بار آن endpoint را ساختهایم.
همان Endpoint معروفی که برای Executive Dashboard یا User Summary استفاده میشود. جایی که باید چندین مجموعه دادهٔ کاملاً نامربوط را بگیری تا یک تصویر کامل به کاربر نشان بدهی.
مثلاً:
• ۵۰ سفارش آخر
• لاگهای سلامت سیستم
• تنظیمات پروفایل کاربر
• تعداد اعلانها
و معمولاً کد را اینگونه مینویسیم:
var orders = await GetRecentOrdersAsync(userId);
var logs = await GetSystemLogsAsync();
var stats = await GetUserStatsAsync(userId);
return new DashboardDto(orders, logs, stats);
این کد کار میکند. تمیز است. خواناست. اما یک مشکل جدی وجود دارد. ⚠️
اگر:
🔹️GetRecentOrdersAsync = 300ms
🔹️GetSystemLogsAsync = 400ms
🔹️GetUserStatsAsync = 300ms
کاربر باید ۱ ثانیه کامل منتظر بماند (۳۰۰ + ۴۰۰ + ۳۰۰).
در سیستمهای توزیعشده، Latency تجربهٔ کاربری را نابود میکند.
و چون این دادهها از هم مستقل هستند، باید بتوانیم آنها را به صورت موازی اجرا کنیم.
اگر این کار را بکنیم، طول کل درخواست = کندترین Query (۴۰۰ms)
یعنی ۶۰٪ بهبود عملکرد فقط با تغییر نحوهٔ اجرای Queryها. 🔥
اما اگر با Entity Framework Core به شکل ساده بخواهی آنها را موازی کنی…
اپلیکیشن Crash میکند.
💥 وعدهٔ دروغین Task.WhenAll
رایجترین اشتباه این است که کد را برداری، آن را در چند Task بیندازی و منتظر بمانی:
// ❌ DO NOT DO THIS
public async Task<DashboardData> GetDashboardData(int userId)
{
var ordersTask = _repository.GetOrdersAsync(userId);
var logsTask = _repository.GetLogsAsync();
var statsTask = _repository.GetStatsAsync(userId);
await Task.WhenAll(ordersTask, logsTask, statsTask); // BOOM 💥
return new DashboardData(ordersTask.Result, logsTask.Result, statsTask.Result);
}
اگر این را اجرا کنی، بلافاصله با این Exception روبرو میشوی:
A second operation started on this context before a previous operation completed.
This is usually caused by different threads using the same instance of DbContext...
چرا این اتفاق میافتد؟ 🤔
❗️چرا DbContext Thread-Safe نیست؟
زیرا DbContext در EF Core stateful است و برای "یک Unit of Work" طراحی شده.
دلایل اصلی:
1️⃣ Change Tracker
ءDbContext وضعیت Entityهایی که بارگذاری شدهاند را نگه میدارد.
دو Thread همزمان نمیتوانند این وضعیت را تغییر دهند.
2️⃣ Single Database Connection
هر DbContext معمولاً یک Connection دارد.
پروتکل پایگاهداده (TCP Stream برای PostgreSQL یا SQL Server) اجازهٔ اجرای همزمان چند Query روی یک Connection را نمیدهد.
3️⃣ ءRace Condition و خراب شدن State داخلی
اگر دو Query همزمان اجرا شوند، DbContext نمیداند کدام نتیجه مربوط به کدام Query است.
بنابراین EF Core برای جلوگیری از فساد داده، Exception میاندازد.
🧩 پس یک تناقض داریم:
میخواهیم Queryها را موازی اجرا کنیم
اما EF Core ما را مجبور میکند پیاپی اجرا کنیم.
در ادامهٔ مقاله راهحلهای درست معرفی میشود…
✅ راهحل
از NET 5. به بعد، EF Core دقیقاً برای همین سناریو یک راهحل درجهیک ارائه داده:
IDbContextFactory<T> 🎉
بهجای تزریق یک DbContext با طول عمر Scoped (که برای کل Request زنده میماند)، یک Factory تزریق میکنیم که هر زمان لازم شد یک DbContext سبک، مستقل و بدون تداخل بسازد.
این یعنی:
هر Task → یک DbContext مستقل → یک Connection مستقل → بدون هیچ Conflict👌
💡نکته:
اگر به DbContextOptions دسترسی داری، حتی میتوانی دستی هم Context بسازی:
using var context = new AppDbContext(options);
🛠 ثبت Factory در Program.cs
ء<IDbContextFactory<AppDbContext به صورت Singleton رجیستر میشود و خود AppDbContext همچنان به صورت Scoped قابل استفاده است
builder.Services.AddDbContextFactory<AppDbContext>(options =>
{
options.UseNpgsql(builder.Configuration.GetConnectionString("db"));
});
⚡️ ءRefactor کردن Dashboard برای اجرای موازی
بهجای اینکه AppDbContext را تزریق کنیم، <IDbContextFactory<AppDbContext را تزریق میکنیم.
در هر Task:
1️⃣ یک DbContext جدید میسازیم
2️⃣ ءQuery را اجرا میکنیم
3️⃣ آن را Dispose میکنیم
بدون هیچ تداخلی 🔥
using Microsoft.EntityFrameworkCore;
public class DashboardService(IDbContextFactory<AppDbContext> contextFactory)
{
public async Task<DashboardDto> GetDashboardAsync(int userId)
{
// 1. اجرای موازی تمام Query ها
var ordersTask = GetOrdersAsync(userId);
var logsTask = GetSystemLogsAsync();
var statsTask = GetUserStatsAsync(userId);
// 2. صبر برای اتمام همه
await Task.WhenAll(ordersTask, logsTask, statsTask);
// 3. برگرداندن نتایج
return new DashboardDto(
await ordersTask,
await logsTask,
await statsTask
);
}
private async Task<List<Order>> GetOrdersAsync(int userId)
{
await using var context = await contextFactory.CreateDbContextAsync();
return await context.Orders
.AsNoTracking()
.Where(o => o.UserId == userId)
.OrderByDescending(o => o.CreatedAt)
.ThenByDescending(o => o.Amount)
.Take(50)
.ToListAsync();
}
private async Task<List<SystemLog>> GetSystemLogsAsync()
{
await using var context = await contextFactory.CreateDbContextAsync();
return await context.SystemLogs
.AsNoTracking()
.OrderByDescending(l => l.Timestamp)
.Take(50)
.ToListAsync();
}
private async Task<UserStats?> GetUserStatsAsync(int userId)
{
await using var context = await contextFactory.CreateDbContextAsync();
return await context.Users
.Where(u => u.Id == userId)
.Select(u => new UserStats { OrderCount = u.Orders.Count })
.FirstOrDefaultAsync();
}
}
🧠 مفاهیم کلیدی
1️⃣ جداسازی کامل Contextها 🧩
هر Task → یک DbContext مستقل
یعنی:
• یک Connection جدا
• بدون رقابت روی Change Tracker
• بدون Race Condition
• بدون Exception خطرناک EF Core
2️⃣ ءDispose شدن سریع Context 🧹
با await using، Context فوراً Dispose میشود و Connection به Connection Pool برمیگردد.
این نکته حیاتی است، مخصوصاً زیر بار بالا.
📊 Benchmark
برای اثبات اینکه این روش واقعاً کار میکند، یک برنامه کوچک NET 10. با Aspire و PostgreSQL ساختم.
چون همهچیز روی سیستم محلی اجرا میشد، زمانها خیلی پاییناند؛
اما اگر دیتابیس ریموت بود، اعداد بزرگتر میشدند — نسبتِ سرعت (Speedup Ratio) تقریباً همین میمانَد.
🐢 اجرای ترتیبی (Sequential Execution): حدود ۳۶ms
در حالت ترتیبی، دقیقاً یک Waterfall داریم:
هر Query منتظر تمامشدن Query قبلی میمانَد.
یعنی:
Query 1 → تمام شود → Query 2 → تمام شود → Query 3
این ساختار از لحاظ تجربهٔ کاربری، فوقالعاده کند است. ⏳
⚡️ اجرای موازی (Parallel Execution): حدود ۱۳ms
با رویکرد موازیسازی:
تمام Query ها همزمان شروع میشوند و تقریباً همزمان به پایان میرسند.
نتیجه:
۳ برابر سریعتر
(در این مثال: ۳۶ms → ۱۳ms)
این همان جایی است که تاخیرهای شبکه، I/O و latency دیتابیس را بهصورت مؤثری کاهش میدهیم. 🚀
⚖️ ملاحظات، Trade-off ها و نتیجهگیری
استفاده از IDbContextFactory پلی است بین:
معماری EF Core که بر واحد کار (Unit of Work) و Context واحد طراحی شده و نیازهای مدرن که اجرای موازی حقیقی را میطلبد.
این الگو به شما اجازه میدهد از جعبهٔ
"یک درخواست → یک Thread → یک DbContext"
خارج شوید — بدون اینکه Thread-Safety یا Data Integrity قربانی شود. ✔️
اما باید با احتیاط استفاده شود:
⚠️ 1️⃣ احتمال اتمام Connection Pool
وقتی درخواست قبلاً فقط ۱ Connection مصرف میکرد،
در حال حاضر همان درخواست ۳ Connection میگیرد.
اگر ترافیک بالایی داشته باشید و connection pool کوچک باشد:
ممکن است سریعاً Connection Exhaustion رخ دهد.
این یعنی درخواستها در صف میمانند یا Timeout میدهند. ❗️
⚠️ 2️⃣ سربار Context
اگر Query های شما بسیار سریع هستند (lookup ساده بر اساس ID)،
ساختن چندین Task و DbContext جدید
ممکن است حتی کندتر از حالت ترتیبی شود!
این روش مناسب Query های سنگینتر و پنجرههای بزرگ داده است—not simple queries. ⚙️
🧠 نکتهٔ پایانی
وقتی با یک Dashboard کند مواجه شدید،
قبل از اینکه سراغ SQL دستی یا Stored Procedure بروید…
نگاهی به await های خود بیندازید.
اگر پشتسرهم صف شدهاند،
وقت آن است که کمی فکر موازی انجام دهید. ⚡️
🔖هشتگها:
#EFCore #ParallelProgramming
#AsyncAwait #SoftwareEngineering #IDbContextFactory
Forwarded from Sonora.Dev
🚀 بهینهسازی پروژههای .NET با Directory.Build.props
اگر روی پروژههای چندپروژهای یا بزرگ در .NET کار میکنید، احتمالاً با مشکلاتی مثل:
ناسازگاری نسخهها
تعریف تنظیمات تکراری برای هر پروژه
مدیریت مسیرهای خروجی و تنظیمات کامپایلر
روبرو شدهاید. اینجاست که Directory.Build.props میتواند ناجی شما باشد!
چرا Directory.Build.props مهم است؟
پیکربندی مرکزی پروژهها: میتوانید تنظیمات عمومی مانند نسخه C# (LangVersion)، مسیر خروجی (OutputPath)، Company و Version را یکجا تعریف کنید.
صرفهجویی در زمان: دیگر نیازی به تغییر دستی تنظیمات در هر پروژه نیست.
ثبات و هماهنگی: تمام پروژههای زیرشاخه از این تنظیمات به ارث میبرند، بنابراین رفتار یکسانی خواهند داشت.
کنترل Warningها و Build Options: میتوانید به راحتی فعال یا غیرفعال کردن Warningها و گزینههای کامپایلر را مدیریت کنید.
نکته عملی:
کافیست یک فایل Directory.Build.props در ریشه Solution ایجاد کنید و Propertyهای مشترک را در آن تعریف کنید. این فایل به صورت خودکار روی تمام پروژههای زیرشاخه اعمال میشود و از تکرار کد و ناسازگاری جلوگیری میکند.
💡 استفاده از Directory.Build.props مخصوصاً در تیمهای بزرگ، باعث سادگی، امنیت و ثبات پروژهها میشود و توسعهدهندگان میتوانند روی نوشتن کد تمرکز کنند، نه تنظیمات پروژه.
#DotNet #CSharp #MSBuild #DirectoryBuildProps #SoftwareDevelopment #BestPractices
اگر روی پروژههای چندپروژهای یا بزرگ در .NET کار میکنید، احتمالاً با مشکلاتی مثل:
ناسازگاری نسخهها
تعریف تنظیمات تکراری برای هر پروژه
مدیریت مسیرهای خروجی و تنظیمات کامپایلر
روبرو شدهاید. اینجاست که Directory.Build.props میتواند ناجی شما باشد!
چرا Directory.Build.props مهم است؟
پیکربندی مرکزی پروژهها: میتوانید تنظیمات عمومی مانند نسخه C# (LangVersion)، مسیر خروجی (OutputPath)، Company و Version را یکجا تعریف کنید.
صرفهجویی در زمان: دیگر نیازی به تغییر دستی تنظیمات در هر پروژه نیست.
ثبات و هماهنگی: تمام پروژههای زیرشاخه از این تنظیمات به ارث میبرند، بنابراین رفتار یکسانی خواهند داشت.
کنترل Warningها و Build Options: میتوانید به راحتی فعال یا غیرفعال کردن Warningها و گزینههای کامپایلر را مدیریت کنید.
نکته عملی:
کافیست یک فایل Directory.Build.props در ریشه Solution ایجاد کنید و Propertyهای مشترک را در آن تعریف کنید. این فایل به صورت خودکار روی تمام پروژههای زیرشاخه اعمال میشود و از تکرار کد و ناسازگاری جلوگیری میکند.
💡 استفاده از Directory.Build.props مخصوصاً در تیمهای بزرگ، باعث سادگی، امنیت و ثبات پروژهها میشود و توسعهدهندگان میتوانند روی نوشتن کد تمرکز کنند، نه تنظیمات پروژه.
#DotNet #CSharp #MSBuild #DirectoryBuildProps #SoftwareDevelopment #BestPractices
50 سؤال برای آمادگی مصاحبه #C 🚀
سؤالات واقعی — نه تولیدشده توسط AI 👇
1️⃣ تفاوت readonly و const چیست؟
2️⃣ کلمه کلیدی sealed برای چه استفاده میشود؟
3️⃣ تمام access modifier های مربوط به type ها را نام ببرید.
4️⃣ تفاوت interface و abstract class چیست؟
5️⃣ چه زمانی static constructor فراخوانی میشود؟
6️⃣ چگونه یک extension method ایجاد میکنیم؟
7️⃣ آیا #C از multiple class inheritance پشتیبانی میکند؟
8️⃣ ءboxing و unboxing را توضیح دهید.
9️⃣ ءheap و stack چیستند؟
🔟 تفاوت string و StringBuilder چیست؟
1️⃣1️⃣ چگونه یک تاریخ با timezone مشخص ایجاد میکنیم؟
2️⃣1️⃣ چگونه current culture را تغییر میدهیم؟
3️⃣1️⃣ تفاوت HashSet و Dictionary چیست؟
4️⃣1️⃣ هدف متد ToLookup چیست؟
5️⃣1️⃣ آیا متد <Cast<T در LINQ یک object جدید ایجاد میکند؟
6️⃣1️⃣ ءdeferred execution در LINQ را توضیح دهید.
7️⃣1️⃣ ءImmutableList چگونه کار میکند؟
8️⃣1️⃣ مزایای استفاده از Frozen Collection ها چیست؟
9️⃣1️⃣ مجموعههای thread-safe را نام ببرید.
0️⃣2️⃣ چگونه برای کد asynchronous یک lock اعمال میکنیم؟
1️⃣2️⃣ تمام روشهای ساخت یک Thread جدید را نام ببرید.
2️⃣2️⃣ چگونه چند async task را همزمان اجرا میکنیم؟
3️⃣2️⃣ ءInheritance در برابر Composition را توضیح دهید.
4️⃣2️⃣ تفاوت class و record و struct چیست؟
5️⃣2️⃣ ءref struct برای چه استفاده میشود؟
6️⃣2️⃣ دو نوع record را نام ببرید.
7️⃣2️⃣ کلمه کلیدی with برای چه استفاده میشود؟
8️⃣2️⃣ هدف Primary Constructor ها چیست؟
9️⃣2️⃣ ءNullable Reference Types چگونه کار میکنند؟
0️⃣3️⃣ آیا switch expression محدودیت نوع بازگشتی دارد؟
1️⃣3️⃣ ءyield return برای چه استفاده میشود؟
2️⃣3️⃣ ءGarbage Collector چند generation دارد؟
3️⃣3️⃣ کلاس Interlocked برای چه استفاده میشود؟
4️⃣3️⃣ کامپایلر برای auto-property ها چه کدی تولید میکند؟
5️⃣3️⃣ ءPolymorphism چگونه در #C پیادهسازی میشود؟
6️⃣3️⃣ ءEncapsulation چگونه در #C پیادهسازی میشود؟
7️⃣3️⃣ تفاوت ref و out چیست؟
8️⃣3️⃣ دستور using چگونه کار میکند؟
9️⃣3️⃣ ءdelegate چیست و چگونه استفاده میشود؟
0️⃣4️⃣ تفاوت overloading و overriding را توضیح دهید.
1️⃣4️⃣ تفاوت IEnumerable و IQueryable چیست؟
2️⃣4️⃣ ءexpression tree ها در LINQ چیستند؟
3️⃣4️⃣ ءException Handling در #C چگونه کار میکند؟
4️⃣4️⃣ تمام روشهای rethrow کردن exception را نام ببرید.
5️⃣4️⃣ ءgenerics را توضیح دهید.
6️⃣4️⃣ تفاوت Auto reset event و Manual reset event چیست؟
7️⃣4️⃣ چگونه کد async را lock میکنیم؟
8️⃣4️⃣ تفاوت volatile و Interlocked چیست؟
9️⃣4️⃣ تمام روشهای ایجاد Thread را نام ببرید.
0️⃣5️⃣ تفاوت Task.Run و TaskFactory.StartNew چیست؟
🔖هشتگها:
#DotNetInterview #ProgrammingInterview #CSharpInterview
12الگوی ضروری طراحی در سیستمهای توزیعشده که هر Architect باید بداند 🌐🚀
ساخت Distributed System کار سادهای نیست.
هرچه سیستمها بزرگتر میشوند و بین چندین سرویس توزیع میشوند، تیمها با چالشهایی مثل Service Communication، Data Consistency، Fault Tolerance و Deployment Strategyها روبهرو میشوند. ⚙️🔥
خبر خوب این است که بسیاری از این چالشها از قبل راهحلهای استاندارد و اثباتشده دارند.
این راهحلها همان Design Patternهای سیستمهای توزیعشدهاند؛ الگوهایی که در میدان عمل بارها تست شدهاند و معماران نرمافزار برای حل مشکلات مشترک معماری از آنها استفاده میکنند. 💡
درک این الگوها کمک میکند:
• تصمیمهای معماری دقیقتری بگیری
• از اشتباهات رایج دوری کنی
• سیستمهایی بسازی که مقاوم، مقیاسپذیر و قابلنگهداری باشند
در این پست، ۱۲ الگوی مهم و کلیدی در سیستمهای توزیعشده را بررسی میکنیم:
1️⃣ API Gateway 🛂
یک نقطه ورودی واحد برای تمام درخواستها که امنیت، Routing، Aggregation و Cross-Cutting Concernها را مدیریت میکند.
2️⃣ Point-To-Point Async Integration 🔄
یک سرویس بهطور مستقیم پیام را برای یک سرویس دیگر ارسال میکند؛ مناسب سناریوهای ساده با coupling بیشتر.
3️⃣ Publish/Subscribe Pattern 📢📨
ءPublisher پیامها را منتشر میکند و Subscriberها بدون وابستگی به ارسالکننده آنها را دریافت میکنند. ایدهآل برای decoupling و Event-Driven Architecture.
4️⃣ Outbox Pattern 📦📝
برای حفظ Consistency بین Database و Message Broker.
اول پیام در Outbox ذخیره میشود؛ بعداً توسط یک پروسس ارسال میگردد → هیچ Eventی گم نمیشود.
5️⃣ CQRS (Command Query Responsibility Segregation) ⚔️
جداسازی مسیر Query (فقط خواندنی) از Command (تغییر وضعیت).
در سیستمهای پیچیده کارایی و خوانایی را افزایش میدهد.
6️⃣ Saga Pattern 🎭🧩
برای مدیریت Distributed Transactionها بدون استفاده از دو-phase commit.
در صورت Failure، عملیات جبرانی اجرا میشود.
7️⃣ Sidecar Pattern 🧳
قرار دادن یک Container کوچک کنار سرویس اصلی برای کارهایی مثل Proxy، Observability، Config و امنیت.
در Service Meshها حیاتی است.
8️⃣ Strangler Fig Pattern 🌱➡️🌳
یک سیستم قدیمی بهتدریج با بخشهای جدید جایگزین میشود.
مهاجرت بدون downtime.
9️⃣ Anti-Corruption Layer Pattern 🛡
لایهای که سیستم جدید را از مدل قدیمی جدا میکند تا domain جدید آلوده نشود.
1️⃣0️⃣ Service Discovery Pattern 🔍
سرویسها بدون نیاز به hardcoded URL همدیگر را پیدا میکنند (Eureka، Consul، Envoy).
1️⃣1️⃣ Sharding Pattern 🧩📊
تقسیم دادهها بین چند دیتابیس برای افزایش مقیاسپذیری و کاهش load.
1️⃣2️⃣ Replication Pattern 📚🔁
کپیکردن دادهها روی چند Node برای دسترسپذیری بالا و fault tolerance.
1️⃣ API Gateway 🛂🚪(بخش اول)
ءAPI Gateway یک نقطهٔ ورودی واحد است که بین Clientها و سرویسهای Backend قرار میگیرد.
این کامپوننت در نقش Reverse Proxy عمل میکند و درخواستها را به Microservice مناسب Route میکند. علاوهبراین، بسیاری از Cross-Cutting Concernها مثل احراز هویت، Rate Limiting، و Transform کردن درخواستها را مدیریت میکند.
نحوهٔ کارکرد API Gateway ⚙️📨
تمام درخواستهای Client به یک Endpoint واحد (API Gateway) ارسال میشود، نه به تکتک سرویسها
ءGateway پس از دریافت درخواست، عملیات Authentication و Authorization را انجام میدهد
قوانین Rate Limiting و Throttling اعمال میشود تا سرویسها زیر فشار از کار نیفتند
ءGateway با توجه به مسیر URL یا Headerها، Request را به سرویس مناسب Route میکند
میتواند پاسخ چند سرویس را جمعآوری و به یک Response تجمیعشده برای Client تبدیل کند
مزایا ✨
🔹️سادهسازی سمت Client با ارائهٔ یک Endpoint واحد
🔹️ایجاد لایهٔ انتزاعی برای تغییر سرویسهای Backend بدون اثرگذاری روی Client
🔹️بهبود امنیت با مخفی کردن ساختار داخلی سرویسها و Endpointهای واقعی آنها
معایب ⚠️
🔸️تبدیل شدن به Single Point of Failure در صورت طراحی غلط
🔸️احتمال تبدیل شدن به Bottleneck در ترافیکهای بالا
🔸️افزودن Latency به هر درخواست بهخاطر یک Roundtrip اضافی شبکه
موارد استفاده 🎯
• معماریهای Microservices که نیاز به یک رابط یکپارچه برای چند سرویس مختلف دارند
• سیستمهایی که Authentication و Authorization یکپارچه نیاز دارند
• مدرنسازی سیستمهای Legacy با پنهان کردن سرویسهای قدیمی پشت یک API مدرن
تفاوت Task و ValueTask چیه؟ 🤔⚡️
خیلیها فکر میکنن ValueTask فقط یک نسخه سبکتر از Task هست…
ولی داستان خیلی جالبتره!
بیاین با یک مثال واقعی درکش کنیم 👇
📌 ءTask چیه؟
🔹️ءTask یعنی:
«یک عملیات async که ممکنه هنوز انجام نشده باشه.»
در NET.، حتی اگر عملیات نتیجهای داشته باشه، یه Task جدید ساخته میشه.
این ساخت Task هزینه داره:
• یک Object جدید ساخته میشه
• ءGC باید بعداً جمعش کنه
• ءMemory Allocation رخ میده
در عملیات سبک و پرتکرار؟
این Allocations میتونه پرفورمنس رو زمین بزنه.
📌 ءValueTask چیه؟
ءValueTask اومده همین مشکل رو حل کنه!
🔸️ءValueTask یعنی:
«یک عملیات async که ممکنه نتیجه رو همین الان داشته باشه.»
اگر نتیجه آماده باشه، ValueTask هیچ object اضافهای ایجاد نمیکنه
• یعنی Zero Allocation
• یعنی سرعت بیشتر
• یعنی فشار کمتر روی GC
به همین دلیل یکی از سریعترین Primitiveهای داتنته.
💥 یک مثال واقعی
تصور کن یک Cache داری:
public Task<Item> GetItemAsync(string key)
{
if (_cache.TryGetValue(key, out var item))
return Task.FromResult(item); // Allocates
return FetchFromDbAsync(key);
}
در حالت بالا حتی برای cache hit هم یک Task جدید ساخته میشه!
ولی با ValueTask:
public ValueTask<Item> GetItemAsync(string key)
{
if (_cache.TryGetValue(key, out var item))
return new ValueTask<Item>(item); // No allocation
return new ValueTask<Item>(FetchFromDbAsync(key));
}
نتیجه؟
در حالت cache hit → هیچ Task اضافهای ساخته نمیشود.
⚠️ اما ValueTask همیشه بهتر نیست!
نکات مهم:
1️⃣ فقط یک بار میتونی awaitش کنی
چون value-type هست
اگر چند بار await کنی → Undefined Behavior
2️⃣ اگر عملیات همیشه async باشه ValueTask فقط پیچیدگی اضافه میکنه. Task مناسبتره.
3️⃣ ءchaining پیچیدهتره
ءConfigureAwait، ContinueWith و … روی ValueTask رفتارهای خاص دارند.
🎯 کی Task؟ کی ValueTask؟
🔸️ از Task استفاده کن وقتی:🔸️
عملیات همیشه async هست
مثل: query دیتابیس، ارسال ایمیل، فراخوانی API
نتیجه زود آماده نمیشود
سرویس ساده و خوانایی مهمتر از پرفورمنسه
🔸️ از ValueTask استفاده کن وقتی:🔸️
عملیات گاهی sync و گاهی async است
مانند: Cache, MemoryReader, CPU-bound operations
عملیات پرتکرار است و performance حیاتی است
میخواهی مخصــوصاً Allocation را صفر کنی
📝 جمعبندی
ءTask → همیشه async → همیشه object جدید → هزینه بیشتر
ءValueTask → async یا sync → گاهی بدون object → سریعتر
اما ValueTask ابزار پیشرفتهست.
نسبت به Task مسئولیت بیشتری روی دوش برنامهنویسه میذاره.
پیشنهاد مایکروسافت:
“Only use ValueTask when performance measurements justify it.”
2️⃣ Point To Point Async Integration ⚡️📩(بخش دوم)
ءPoint To Point Async Integration یک الگوی ارتباطی هست که در آن یک سرویس، پیامها را از طریق یک Message Queue برای سرویس دیگر ارسال میکند.
برخلاف ارتباط synchronous، فرستنده منتظر پاسخ نمیماند و به کار خودش ادامه میدهد؛ درحالیکه سرویس گیرنده پیامها را با سرعت و زمانبندی خودش مصرف میکند.
این موضوع یک رابطه غیرهمزمان اما همچنان مستقیم بین دو سرویس ایجاد میکند.
🔧 How it works:
🔸️ءService A پیام را به یک صف اختصاصی که فقط Service B از آن مصرف میکند ارسال میکند
🔹️ءMessage Queue بهعنوان بافر بین فرستنده و گیرنده عمل میکند
🔸️ءService A بلافاصله بعد از ارسال پیام ادامه پروسه را انجام میدهد
🔹️ءService B پیامها را در زمان موردنظر خودش مصرف میکند
🔸️ءMessage Broker تضمین میکند که پیامها بهدرستی و قابلاعتماد ارسال شوند
🔹️اگر سرویـس B موقتاً در دسترس نباشد، پیامها در صف باقی میمانند
🔸️ءDead Letter Queue برای پیامهایی که بعد از چند تلاش دوباره fail میشوند استفاده میشود
✅ Benefits:
• ءDecouples services in time → سرویسها میتوانند با سرعتهای متفاوت کار کنند
• ءResilience بالاتر → قطع بودن Service B روی A تأثیری ندارد
• ءLoad leveling طبیعی از طریق صف
• ءAsync processing برای کارهای طولانی بدون بلاک کردن سرویس فرستنده
• ءScalability آسان با افزودن Consumerهای بیشتر
❌ Drawbacks:
• ءMessage Broker یک وابستگی اضافه است و ممکن است Single Point of Failure ایجاد کند
• منجر به Eventual Consistency میشود
• ءDebugging سختتر بهدلیل جریان غیرهمزمان پیامها
🎯 Use cases:
1️⃣ءBackground Job Processing
2️⃣سیستمهای پردازش سفارش (Order Fulfillment)
3️⃣ءEmail / Notification Services
4️⃣سیستمهایی با Load نامتوازن که Queue نقش بافر ایفا میکند