C# Geeks (.NET) – Telegram
چرا YARP را انتخاب کنیم؟

مزایای منحصر به فردی ارائه می‌دهد که آن را برای توسعه‌دهندگان جذاب می‌کند:

🔹️Customization: YARP
بسیار قابل تنظیم است و توسعه‌دهندگان می‌توانند پروکسی را با حداقل تلاش به نیازهای خود تطبیق دهند

🔸️Integration with .NET:
بر پایه ASP.NET Core ساخته شده و به خوبی با اکوسیستم NET. یکپارچه می‌شود

🔹️Extensibility:
نقاط توسعه‌ی گسترده برای افزودن منطق و ویژگی‌های سفارشی با استفاده از کد #C فراهم می‌کند

🔸️Scalability:
قابلیت direct forwarding extensibility امکان مقیاس‌پذیری نام دامنه و سرورهای بک‌اند را فراهم می‌کند، چیزی که با اکثر reverse proxy‌ها ممکن نیست

🔹️Active development: YARP
به طور فعال توسط مایکروسافت نگهداری و توسعه داده می‌شود
🔸️Comprehensive maintained documentation:
مستندات جامع و مثال‌ها، شروع سریع و پیاده‌سازی ویژگی‌های پیشرفته را آسان می‌کند

🔹️Open source:
و مستندات آن متن‌باز هستند و مشارکت، بازبینی و بازخورد پذیرفته می‌شود 💻

🔖هشتگ‌ها:
#YARP #ReverseProxy
The Idempotent Consumer Pattern in .NET
🧩 الگوی Idempotent Consumer در NET. (و چرا به آن نیاز دارید)

سیستم‌های توزیع‌شده ذاتاً غیرقابل اعتماد هستند. ⚠️

من همیشه توصیه می‌کنم برای درک بهتر اشتباهات رایج، مقاله‌ی معروف Fallacies of Distributed Computing را مطالعه کنید.

یکی از چالش‌های کلیدی در این سیستم‌ها، اطمینان از این است که هر پیام دقیقاً یک‌بار پردازش شود که از نظر تئوری در بیشتر سیستم‌ها غیرممکن است. 😅

در اینجا وارد مباحثی مثل CAP Theorem یا Two Generals Problem نمی‌شویم، اما کافی است بدانید که در دنیای واقعی:

پیام‌ها ممکن است خارج از ترتیب برسند 🌀
پیام‌ها ممکن است تکراری شوند 🔁
تحویل پیام‌ها ممکن است با تأخیر انجام شود 🕒

اگر سیستم خود را طوری طراحی کنید که فرض کند هر پیام دقیقاً یک‌بار پردازش می‌شود، در واقع دارید زمینه را برای خرابی داده‌های پنهان فراهم می‌کنید. 💥

اما می‌توانیم سیستم خود را طوری طراحی کنیم که اثرات جانبی (side effects) فقط یک‌بار اعمال شوند با استفاده از الگوی قدرتمند Idempotent Consumer. 💡

بیایید با هم بررسی کنیم:
🔹 چه خطاهایی ممکن است رخ دهند
🔹 چگونه message brokerها در مدیریت idempotency کمک می‌کنند
🔹 و چطور می‌توانیم یک Idempotent Consumer در NET. بسازیم

🚨 چه چیزی ممکن است هنگام انتشار پیام اشتباه پیش برود؟

فرض کنید سرویس شما زمانی که یک یادداشت جدید ایجاد می‌شود، یک event منتشر می‌کند:
await publisher.PublishAsync(new NoteCreated(note.Id, note.Title, note.Content));

در اینجا اهمیتی ندارد که publisher یا message broker شما چه پیاده‌سازی‌ای دارد می‌تواند RabbitMQ، SQS، یا Azure Service Bus باشد.

حالا تصور کنید سناریوی زیر رخ دهد:

• Publisher
پیام را به broker ارسال می‌کند

• Broker
پیام را ذخیره کرده و یک ACK (تأیید دریافت) برمی‌گرداند

• یک اختلال شبکه باعث می‌شود ACK هرگز به producer نرسد 🌐

• Producer timeout
می‌شود و انتشار را دوباره تلاش می‌کند 🔁

حالا broker دو event از نوع NoteCreated دارد 😬

از دید producer، فقط یک timeout رفع شده است.
اما از دید consumer، دو پیام دریافت شده که هر دو مربوط به ایجاد یک یادداشت هستند.

و این تنها یکی از مسیرهای خرابی ممکن است!

ممکن است پیام‌های تکراری به دلایل زیر هم اتفاق بیفتند:

• ارسال مجدد توسط broker
• خرابی consumer و اجرای مجدد در retryها

بنابراین حتی اگر در سمت publisher همه چیز را “درست” انجام دهید، باز هم consumer باید محافظه‌کارانه طراحی شود تا در برابر تکرارها مقاوم باشد. 🧱
⚙️ Publisher-Side Idempotency (بگذارید Broker آن را مدیریت کند)

بسیاری از message brokerها از قبل از طریق قابلیت message deduplication، از انتشار idempotent پشتیبانی می‌کنند؛ البته اگر در پیام خود یک شناسه‌ی یکتا (unique message ID) قرار دهید.

به‌عنوان مثال، Azure Service Bus می‌تواند پیام‌های تکراری را تشخیص داده و انتشار مجدد پیام‌هایی با MessageId یکسان را در بازه‌ی زمانی مشخص‌شده نادیده بگیرد.Amazon SQS و سایر brokerها نیز تضمین‌های مشابهی ارائه می‌دهند. 💪

شما نیازی ندارید این منطق را دوباره در برنامه‌ی خود پیاده‌سازی کنید.
نکته‌ی کلیدی این است که برای هر پیام، یک شناسه‌ی پایدار (stable identifier) اختصاص دهید که به‌طور یکتا رویداد منطقی مورد نظر را نمایش دهد.

به‌عنوان مثال، هنگام انتشار یک event از نوع NoteCreated:
var message = new NoteCreated(note.Id, note.Title, note.Content)
{
MessageId = Guid.NewGuid() // یا می‌توانید از note.Id استفاده کنید
};

await publisher.PublishAsync(message);


اگر شبکه پس از ارسال پیام قطع شود 🌐، ممکن است برنامه‌ی شما ارسال را مجدداً تلاش کند (retry).
اما زمانی که broker همان MessageId را مشاهده کند، متوجه می‌شود که این پیام تکراری است و آن را به‌صورت ایمن نادیده می‌گیرد.

به این ترتیب، شما deduplication را بدون نیاز به جداول ردیابی (tracking tables) یا state اضافی در سرویس خود به‌دست می‌آورید.

این نوع idempotency در سطح broker، بخش بزرگی از مشکلات سمت producer را حل می‌کند مثل:

• retryهای شبکه 🔁
• خطاهای موقتی ⚠️
• انتشارهای تکراری 🌀

اما چیزی که این مکانیسم پوشش نمی‌دهد، retryهای سمت consumer است یعنی زمانی که پیام‌ها مجدداً تحویل داده می‌شوند یا سرویس شما هنگام پردازش دچار crash می‌شود 💥.

اینجاست که الگوی Idempotent Consumer وارد عمل می‌شود. 🎯

🧠 Implementing an Idempotent Consumer in .NET

در اینجا یک نمونه از Idempotent Consumer برای eventی از نوع NoteCreated آورده شده است:
internal sealed class NoteCreatedConsumer(
TagsDbContext dbContext,
HybridCache hybridCache,
ILogger<Program> logger) : IConsumer<NoteCreated>
{
public async Task ConsumeAsync(ConsumeContext<NoteCreated> context)
{
// 1. بررسی اینکه آیا این پیام قبلاً توسط این consumer پردازش شده است
if (await dbContext.MessageConsumers.AnyAsync(c =>
c.MessageId == context.MessageId &&
c.ConsumerName == nameof(NoteCreatedConsumer)))
{
return;
}

var request = new AnalyzeNoteRequest(
context.Message.NoteId,
context.Message.Title,
context.Message.Content);

try
{
using var transaction = await dbContext.Database.BeginTransactionAsync();

// 2. پردازش قطعی (Deterministic): استخراج تگ‌ها از محتوای یادداشت
var tags = AnalyzeContentForTags(request.Title, request.Content);

// 3. ذخیره‌سازی تگ‌ها در پایگاه داده
var tagEntities = tags.Select(ProjectToTagEntity(request.NoteId)).ToList();
dbContext.Tags.AddRange(tagEntities);

// 4. ثبت اینکه این پیام پردازش شده است
dbContext.MessageConsumers.Add(new MessageConsumer
{
MessageId = context.MessageId,
ConsumerName = nameof(NoteCreatedConsumer),
ConsumedAtUtc = DateTime.UtcNow
});

await dbContext.SaveChangesAsync();
await transaction.CommitAsync();

// 5. به‌روزرسانی Cache
await CacheNoteTags(request, tags);
}
catch (Exception ex)
{
logger.LogError(ex, "Error analyzing note {NoteId}", request.NoteId);
throw;
}
}
}

این یک نمونه‌ی متداول از Idempotent Consumer است که شامل چند نکته‌ی کلیدی است 🧩
🧱 1️⃣ The Idempotency Key

if (await dbContext.MessageConsumers.AnyAsync(c =>
c.MessageId == context.MessageId &&
c.ConsumerName == nameof(NoteCreatedConsumer)))
{
return;
}

در اینجا از موارد زیر استفاده می‌کنیم:

MessageId
که از transport (یعنی context.MessageId) گرفته می‌شود

ConsumerName
تا در صورتی که چند consumer متفاوت یک پیام را پردازش کنند، هرکدام به‌صورت ایمن عمل کنند

اگر یک پیام تکراری دریافت شود، پردازش کوتاه می‌شود و هیچ کاری انجام نمی‌گیرد. 🚫

نکته‌ی بسیار مهم این است که باید روی ستون‌های (MessageId, ConsumerName) در جدول MessageConsumers یک unique constraint تعریف شود تا از race condition جلوگیری شود ⚙️
به این ترتیب حتی اگر چند پردازش هم‌زمان از یک پیام وجود داشته باشد، فقط یکی از آن‌ها موفق به درج رکورد خواهد شد. 💪

⚡️ 2️⃣ Atomic Side Effects + Idempotency Record

در این الگو، هم پردازش (processing) و هم ذخیره‌ی رکورد MessageConsumer در یک تراکنش (transaction) انجام می‌شود:
using var transaction = await dbContext.Database.BeginTransactionAsync();

// write tags
dbContext.Tags.AddRange(tagEntities);

// write message-consumer record
dbContext.MessageConsumers.Add(new MessageConsumer { ... });

await dbContext.SaveChangesAsync();
await transaction.CommitAsync();


چرا این مهم است؟ 🤔

اگر پردازش شکست بخورد ، هیچ ورودی‌ای در MessageConsumers ثبت نمی‌شود، بنابراین پیام می‌تواند مجدداً retry شود.

اگر پردازش موفق باشد ، هم داده‌ها (مثل tags) و هم رکورد مربوط به پیام باهم commit می‌شوند.

در نتیجه، هیچ‌وقت در وضعیتی قرار نمی‌گیرید که کار انجام شده باشد اما پیام به‌عنوان پردازش‌شده علامت‌گذاری نشده باشد، یا برعکس.
این اساس idempotency است:

انجام دقیق یک‌بار عملیات برای هر Message ID — حتی در شرایط retry. 🔁

📬 3️⃣ Handling At-Least-Once Delivery

در بیشتر سناریوهای واقعی، تحویل پیام‌ها از نوع at-least-once است:

1️⃣ Consumer پیام را پردازش می‌کند
2️⃣ ACK شکست می‌خورد یا timeout می‌شود
3️⃣ Broker پیام را مجدداً تحویل می‌دهد
4️⃣ کد شما دوباره اجرا می‌شود

اما در این الگو، اجرای دوم با بررسی جدول MessageConsumers مواجه شده و خیلی سریع return می‌کند.

نتیجه:
هیچ side effect تکراری‌ای اتفاق نمی‌افتد. 🙌
البته فقط یک استثنا وجود دارد...

🔄 Deterministic vs Non-Deterministic Handlers

وقتی handler شما با سیستم‌هایی خارج از دیتابیس تماس می‌گیرد چه می‌شود؟
مثل:

• یک Email API ✉️
• Payment Gateway 💳
• یا یک Background Job Queue 🧵
این‌ها همگی side effectهای رایج هستند که باید آن‌ها نیز idempotent باشند.

چون این تماس‌ها خارج از محدوده‌ی تراکنش دیتابیس انجام می‌شوند، ممکن است دیتابیس commit شود اما به‌دلیل اختلال شبکه پاسخ از سرویس بیرونی برنگردد.
در retry بعدی، ممکن است همان ایمیل دوباره ارسال شود یا همان کارت اعتباری دوباره شارژ شود ⚠️

به این ترتیب، وارد قلمروی Non-Deterministic Handlerها می‌شویم عملیاتی که تکرار آن‌ها ایمن نیست.

دو استراتژی اصلی برای مدیریت این وضعیت وجود دارد:

🧩 1. استفاده از Idempotency Key در فراخوانی خارجی

اگر سرویس خارجی از Idempotency Key پشتیبانی کند، یک شناسه‌ی پایدار مثلاً همان MessageId پیام را در هر درخواست ارسال کنید.

بسیاری از APIها (مثل پردازشگرهای پرداخت یا پلتفرم‌های ارسال ایمیل) اجازه می‌دهند که یک Idempotency-Key Header مشخص کنید.
در این صورت سرویس تضمین می‌کند که درخواست‌های تکراری با کلید یکسان فقط یک‌بار اجرا شوند.

به‌عنوان مثال:
await emailService.SendAsync(new SendEmailRequest
{
To = user.Email,
Subject = "Welcome!",
Body = "Thanks for signing up.",
IdempotencyKey = context.MessageId
});

حتی اگر درخواست مجدداً ارسال شود، provider کلید را تشخیص می‌دهد و درخواست تکراری را نادیده می‌گیرد.
این ساده‌ترین و مطمئن‌ترین روش است، اگر وابستگی خارجی شما از آن پشتیبانی کند. 🚀

💾 2. ذخیره‌ی Intent به‌صورت محلی

اگر سرویس خارجی از idempotency key پشتیبانی نکند، می‌توانید آن را شبیه‌سازی کنید.
کافی است پیش از تماس با سرویس بیرونی، رکوردی از اقدام مورد نظر را در دیتابیس ذخیره کنید.

مثلاً جدولی به نام PendingEmails بسازید که نشان دهد کدام پیام باید ارسال شود بر اساس MessageId یا UserId.

سپس یک background process این رکوردها را خوانده و عملیات را تنها یک‌بار انجام می‌دهد.
این رویکرد deterministic است ولی پیچیدگی بیشتری دارد (جداول بیشتر و workerهای پس‌زمینه).
معمولاً تنها در موارد حیاتی یا غیرقابل بازگشت مثل پرداخت‌ها یا provisioning حساب‌ها به این میزان از اطمینان نیاز است. 💳

⚖️ The Trade-Off

این تصمیم بستگی به سطح اطمینان مورد نیاز دارد:

اگر تکرار عملیات عواقب واقعی دارد (مالی یا داده‌ای)، باید idempotency را صریحاً اعمال کنید.
در غیر این صورت، retry کردن عملیات ممکن است قابل‌قبول باشد.

🧠 When Idempotent Consumer Isn’t Needed

همه‌ی consumerها به سربار بررسی‌های idempotency نیاز ندارند.

اگر عملیات شما به‌طور طبیعی idempotent است، می‌توانید از جدول اضافه و تراکنش صرف‌نظر کنید.

به‌عنوان مثال:

به‌روزرسانی projectionها 📊
تنظیم flag وضعیت
یا refresh کردن cache 🧩

همگی نمونه‌هایی از عملیات deterministic هستند که اجرای چندباره‌ی آن‌ها خطری ندارد.
مثل:
«تنظیم وضعیت کاربر روی Active» یا «بازسازی Read Model» — این‌ها state را بازنویسی می‌کنند نه اینکه چیزی به آن اضافه کنند.

برخی از handlerها هم از Precondition Check برای جلوگیری از تکرار استفاده می‌کنند.
اگر handler در حال به‌روزرسانی یک entity باشد، ابتدا می‌تواند بررسی کند که آیا entity در وضعیت مورد نظر هست یا نه؛ اگر هست، به‌سادگی return کند.

این محافظ ساده در بسیاری موارد کافی است.

⚠️ الگوی Idempotent Consumer را بدون فکر در همه‌جا اعمال نکنید.
فقط در جایی از آن استفاده کنید که از آسیب واقعی (مالی یا ناسازگاری داده‌ای) جلوگیری کند.
برای سایر موارد، سادگی بهتر است.

🧭 Takeaway

سیستم‌های توزیع‌شده ذاتاً غیرقابل‌پیش‌بینی هستند. ⚙️ Retry‌ها، پیام‌های تکراری (duplicates) و خرابی‌های جزئی (partial failures) بخش طبیعی عملکرد آن‌ها محسوب می‌شوند.
نمی‌توانی از وقوعشان جلوگیری کنی، اما می‌توانی سیستم را طوری طراحی کنی که کمترین تأثیر را از آن‌ها بگیرد. 💪

از قابلیت Message Deduplication داخلی در Broker خود استفاده کن تا پیام‌های تکراری از سمت Producer حذف شوند.
در سمت Consumer، الگوی Idempotent Consumer Pattern را اعمال کن تا مطمئن شوی Side Effectها فقط یک‌بار رخ می‌دهند حتی در صورت Retry شدن پیام‌ها. 🔁

همیشه رکورد پیام‌های پردازش‌شده و اثر واقعی آن‌ها را در یک تراکنش واحد ذخیره کن.
این کار کلید حفظ Consistency در سیستم توزیع‌شده است. 🧱

نه هر Message Handler‌ی نیاز به این الگو دارد.
اگر Consumer شما ذاتاً Idempotent است یا می‌تواند با یک Precondition ساده پردازش را زود متوقف کند، نیازی به پیچیدگی اضافی نیست. 🚫

اما هرجایی که عملیات باعث تغییر Persistent State یا فراخوانی سیستم‌های خارجی می‌شود، Idempotency دیگر یک انتخاب نیست — بلکه تنها راه تضمین Consistency است.

سیستم خود را طوری بساز که Retryها را تحمل کند.
در این صورت، سیستم توزیع‌شده‌ات بسیار قابل‌اعتمادتر خواهد شد. 🔐

نکته‌ی جالب اینجاست که وقتی این اصل را واقعاً درک می‌کنی، آن را در همه‌ی سیستم‌های واقعی دنیا می‌بینی. 🌍

امیدوارم این مطلب برات مفید بوده باشه 💙

🔖هشتگ‌ها:
#IdempotentConsumer #Idempotency #DistributedSystems #MessageBroker
Horizontally Scaling ASP.NET Core APIs With YARP Load Balancing✨️
⚖️ مقیاس‌پذیری افقی (Horizontally Scaling) در ASP.NET Core APIs با استفاده از YARP Load Balancing
اپلیکیشن‌های وب مدرن باید بتوانند تعداد فزاینده‌ای از کاربران را سرویس‌دهی کنند و افزایش ناگهانی ترافیک را مدیریت نمایند.
زمانی که یک سرور به حد نهایی ظرفیت خود می‌رسد، عملکرد آن کاهش یافته و منجر به کندی پاسخ‌ها، خطاها یا حتی از کار افتادن کامل (downtime) می‌شود.

Load Balancing
یک تکنیک کلیدی برای مقابله با این چالش‌ها و بهبود Scalability اپلیکیشن شما است. ⚙️

در این مقاله بررسی خواهیم کرد:

• چگونه از YARP (Yet Another Reverse Proxy) برای پیاده‌سازی Load Balancing استفاده کنیم

• چگونه از Horizontal Scaling برای بهبود عملکرد بهره ببریم

• چگونه از K6 به‌عنوان ابزار Load Testing استفاده کنیم

در ادامه، به مفهوم Load Balancing، اهمیت آن و نحوه‌ای که YARP این فرآیند را برای اپلیکیشن‌های NET. ساده‌تر می‌کند، خواهیم پرداخت.

🧱 انواع مقیاس‌پذیری نرم‌افزار (Types of Software Scalability)

قبل از اینکه وارد جزئیات YARP و Load Balancing شویم، بیایید اصول اولیه‌ی Scaling را مرور کنیم.

دو رویکرد اصلی برای مقیاس‌پذیری وجود دارد:
🔹 Vertical Scaling

در این روش، سرورهای موجود با سخت‌افزار قوی‌تر ارتقا می‌یابند — افزایش تعداد هسته‌های CPU، حافظه RAM و فضای ذخیره‌سازی سریع‌تر.
اما این روش چند محدودیت دارد:
هزینه‌ها به‌سرعت افزایش می‌یابد و در نهایت به یک سقف عملکرد (performance ceiling) خواهید رسید.

🔹 Horizontal Scaling

در این روش، سرورهای بیشتری به زیرساخت خود اضافه می‌کنید و بار کاری را به‌صورت هوشمندانه میان آن‌ها توزیع می‌کنید.
این رویکرد پتانسیل مقیاس‌پذیری بسیار بیشتری دارد، زیرا می‌توانید با افزودن سرورهای جدید، ترافیک بیشتر را مدیریت کنید.

اینجاست که Load Balancing وارد عمل می‌شود — و YARP در این زمینه به‌خوبی می‌درخشد.

🧭 افزودن یک Reverse Proxy

YARP
یک کتابخانه‌ی Reverse Proxy با کارایی بالا از مایکروسافت است.این ابزار برای معماری‌های مدرن microservice طراحی شده است.Reverse Proxy در جلوی سرورهای backend شما قرار می‌گیرد و نقش مدیر ترافیک (traffic director) را ایفا می‌کند. 🚦

راه‌اندازی YARP بسیار ساده است:


• پکیج YARP NuGet را نصب می‌کنید،
• یک تنظیم ساده برای تعریف مقاصد backend می‌سازید،
• و سپس YARP middleware را فعال می‌کنید.

YARP
این امکان را می‌دهد تا قبل از رسیدن درخواست‌ها به سرورهای backend، مسیر‌یابی (routing) و تبدیل (transformation) روی آن‌ها انجام دهید.

🧩 مرحله‌ی اول: نصب پکیج YARP
Install-Package Yarp.ReverseProxy


⚙️ مرحله‌ی دوم: پیکربندی سرویس‌ها و افزودن Middleware

در این مرحله، سرویس‌های موردنیاز را پیکربندی کرده و YARP middleware را به Request Pipeline معرفی می‌کنیم:
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();

app.MapReverseProxy();

app.Run();


🧾 مرحله‌ی سوم: افزودن تنظیمات YARP در appsettings.json

در این فایل، YARP از مفهوم Routes برای نمایش درخواست‌های ورودی به Reverse Proxy و از Clusters برای تعریف سرویس‌های پایین‌دستی (downstream services) استفاده می‌کند.
الگوی {**catch-all} به ما اجازه می‌دهد تمام درخواست‌های ورودی را به‌راحتی مسیر‌دهی کنیم.
{
"ReverseProxy": {
"Routes": {
"api-route": {
"ClusterId": "api-cluster",
"Match": {
"Path": "{**catch-all}"
},
"Transforms": [{ "PathPattern": "{**catch-all}" }]
}
},
"Clusters": {
"api-cluster": {
"Destinations": {
"destination1": {
"Address": "http://api:8080"
}
}
}
}
}
}

این پیکربندی، YARP را به‌صورت یک Pass-through Proxy تنظیم می‌کند.
اما حالا بیایید آن را به‌روزرسانی کنیم تا از مقیاس‌پذیری افقی (Horizontal Scaling) پشتیبانی کند. 🚀
⚙️ Scaling Out با YARP Load Balancing

هسته‌ی اصلی مقیاس‌پذیری افقی (Horizontal Scaling) با استفاده از YARP در استراتژی‌های مختلف Load Balancing آن نهفته است.YARP چندین Load Balancing Strategy مختلف ارائه می‌دهد که هر کدام رفتار متفاوتی در توزیع درخواست‌ها دارند:

⚖️ PowerOfTwoChoices:
دو مقصد تصادفی را انتخاب می‌کند و درخواستی را به سروری ارسال می‌کند که کمترین تعداد درخواست تخصیص داده‌شده را دارد.
🔤 FirstAlphabetical:
اولین سرور در دسترس را بر اساس ترتیب الفبایی انتخاب می‌کند.
🔁 LeastRequests:
درخواست‌ها را به سرورهایی ارسال می‌کند که کمترین تعداد درخواست فعال دارند.

🔄 RoundRobin:
درخواست‌ها را به‌صورت یکنواخت بین تمام سرورهای backend توزیع می‌کند.

🎲 Random:
برای هر درخواست، به‌صورت تصادفی یک سرور backend را انتخاب می‌کند.

شما می‌توانید این استراتژی‌ها را در فایل تنظیمات YARP پیکربندی کنید.
استراتژی Load Balancing با استفاده از ویژگی LoadBalancingPolicy در بخش Cluster مشخص می‌شود.

🧩 پیکربندی YARP با RoundRobin Load Balancing

در ادامه، نسخه‌ی به‌روزشده‌ی فایل تنظیمات YARP را می‌بینید که از استراتژی RoundRobin برای توزیع بار استفاده می‌کند:
{
"ReverseProxy": {
"Routes": {
"api-route": {
"ClusterId": "api-cluster",
"Match": {
"Path": "{**catch-all}"
},
"Transforms": [{ "PathPattern": "{**catch-all}" }]
}
},
"Clusters": {
"api-cluster": {
"LoadBalancingPolicy": "RoundRobin",
"Destinations": {
"destination1": {
"Address": "http://api-1:8080"
},
"destination2": {
"Address": "http://api-2:8080"
},
"destination3": {
"Address": "http://api-3:8080"
}
}
}
}
}
}


🧭 ساختار سیستم با YARP Load Balancer

در تصویر، ساختار کلی سیستم ما با یک YARP Load Balancer و مجموعه‌ای از Application Server‌های مقیاس‌پذیر افقی نشان داده شده است.

درخواست‌های ورودی به API ابتدا به YARP ارسال می‌شوند، و سپس YARP بر اساس استراتژی انتخاب‌شده‌ی Load Balancing، ترافیک را بین سرورهای اپلیکیشن توزیع می‌کند.

در این مثال، یک پایگاه داده (Database) داریم که به چندین نمونه از اپلیکیشن سرویس‌دهی می‌کند. 🗄

🧪 حالا نوبت تست عملکرد است (Performance Testing)

در ادامه، با استفاده از ابزار K6 به بررسی عملکرد سیستم در شرایط بار بالا خواهیم پرداخت تا اطمینان حاصل شود که استراتژی Load Balancing ما به درستی کار می‌کند و مقیاس‌پذیری بهینه حاصل شده است. 🚀
⚡️ Performance Testing با K6

برای مشاهده‌ی تأثیر مقیاس‌پذیری افقی (Horizontal Scaling) در عملکرد سیستم، باید تست بارگیری (Load Testing) انجام دهیم.
ابزار K6 یک ابزار مدرن و کاربرپسند برای Load Testing است که توسط توسعه‌دهندگان به‌راحتی قابل استفاده است.

در این بخش، با نوشتن اسکریپت‌هایی در K6 ترافیک کاربر را روی اپلیکیشن شبیه‌سازی کرده و معیارهایی مثل میانگین زمان پاسخ (Average Response Time) و تعداد درخواست‌های موفق در هر ثانیه (Requests Per Second) را مقایسه خواهیم کرد.

اپلیکیشنی که قرار است به‌صورت افقی مقیاس‌پذیر شود، دارای دو API endpoint است:
• POST /users:
یک کاربر جدید ایجاد می‌کند، آن را در پایگاه داده‌ی PostgreSQL ذخیره کرده و شناسه‌ی کاربر را برمی‌گرداند.

• GET /users/id:
اگر کاربری با شناسه‌ی مشخص وجود داشته باشد، آن را برمی‌گرداند.

🧪 تست عملکرد K6 شامل مراحل زیر است:

• افزایش تدریجی تا ۲۰ کاربر مجازی (Virtual Users)

• ارسال یک درخواست POST به endpoint /users

• بررسی اینکه پاسخ 201 Created بازگردانده شود

• ارسال یک درخواست GET به endpoint /users/{id}

• بررسی اینکه پاسخ 200 OK بازگردانده شود

توجه داشته باشید که تمام درخواست‌های API از طریق YARP Load Balancer عبور می‌کنند. ⚙️
import { check } from 'k6';
import http from 'k6/http';

export const options = {
stages: [
{ duration: '10s', target: 20 },
{ duration: '1m40s', target: 20 },
{ duration: '10s', target: 0 }
]
};

export default function () {
const proxyUrl = 'http://localhost:3000';

const response = http.post(${proxyUrl}/users);

check(response, {
'response code was 201': (res) => res.status == 201
});

const userResponse = http.get(${proxyUrl}/users/${response.body});

check(userResponse, {
'response code was 200': (res) => res.status == 200
});
}

برای اینکه نتایج تست عملکرد سازگارتر باشند، می‌توان منابع قابل‌دسترس در Docker containerها را محدود کرد — مثلاً به ۱ CPU و ۰.۵ گیگابایت RAM:
services:
api:
image: ${DOCKER_REGISTRY-}loadbalancingapi
cpus: 1
mem_limit: '0.5G'
ports:
- 5000:8080
networks:
- proxybackend


🧠 Summary

مقیاس‌پذیری افقی (Horizontal Scaling) در کنار Load Balancing مؤثر می‌تواند به شکل چشمگیری عملکرد و مقیاس‌پذیری اپلیکیشن‌های وب را افزایش دهد.
مزایای مقیاس‌پذیری افقی زمانی بیشتر نمایان می‌شوند که حجم ترافیک بالا برود و یک سرور به‌تنهایی نتواند پاسخگوی نیازها باشد.

و YARP یک Reverse Proxy قدرتمند و کاربرپسند برای اپلیکیشن‌های NET. است.
با این حال، در سیستم‌های بزرگ و پیچیده‌ی Distributed Systems، ممکن است استفاده از راه‌حل‌های اختصاصی Load Balancer گزینه‌ی بهتری باشد — چراکه کنترل دقیق‌تر و قابلیت‌های پیشرفته‌تری ارائه می‌دهند.

🔖هشتگ‌ها:
#YARP #DotNet #LoadBalancing #HorizontalScaling #PerformanceTesting #K6 #ReverseProxy
New Features in .NET 10 and C# 14🔥
🚀 چه چیزهایی در ‎C# 14‎ جدید است

نسخه‌ی ‎C# 14‎ یکی از مهم‌ترین و تأثیرگذارترین نسخه‌های منتشر شده در سال‌های اخیر است.

بیایید نگاهی بیندازیم به ویژگی‌های کلیدی این نسخه: 👇

🧩 Extension Members

ویژگی Extension Members محبوب‌ترین قابلیت جدید من در ‎C# 14‎ است.
این ویژگی در واقع تکامل مدرن مفهوم Extension Methods محسوب می‌شود قابلیتی که از نسخه‌ی ‎C# 3.0‎ معرفی شده بود.

🔹 نحو سنتی Extension Method

قبل از ‎C# 14‎، برای ایجاد Extension Method باید متدهای استاتیک را با پارامتر this می‌نوشتید:
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string value)
{
return string.IsNullOrEmpty(value);
}

public static string Truncate(this string value, int maxLength)
{
if (string.IsNullOrEmpty(value) value.Length <= maxLength)
{
return value;
}

return value.Substring(0, maxLength);
}
}


🔹 نحو جدید با کلیدواژه‌ی ‎extension‎

در ‎C# 14‎ سینتکس جدیدی معرفی شده که receiver (نوعی که می‌خواهید آن را گسترش دهید) را از اعضایی که به آن اضافه می‌کنید جدا می‌کند.
به جای قرار دادن this روی هر پارامتر متد، حالا می‌توانید یک بلوک extension تعریف کنید که فقط یک‌بار نوع receiver را مشخص می‌کند:
public static class StringExtensions
{
extension(string value)
{
public bool IsNullOrEmpty()
{
return string.IsNullOrEmpty(value);
}

public string Truncate(int maxLength)
{
if (string.IsNullOrEmpty(value) value.Length <= maxLength)
return value;

return value.Substring(0, maxLength);
}
}
}

در اینجا بلوک extension نوع receiver را به عنوان پارامتر می‌گیرد. درون این بلوک، می‌توانید متدها و propertyها را طوری بنویسید که انگار واقعاً عضو نوع اصلی هستند.
پارامتر value در تمام اعضای داخل بلوک در دسترس است.

نکته‌ی مهم اینجاست که هر دو سینتکس قدیمی و جدید در نهایت به کد یکسانی کامپایل می‌شوند، بنابراین رفتارشان دقیقاً مشابه است.
حتی می‌توانید هر دو سبک را در یک کلاس استاتیک به‌صورت هم‌زمان استفاده کنید. 💡

⚙️ پشتیبانی‌های جدید در نحو ‎extension‎

نحو جدید ‎extension‎ از موارد زیر پشتیبانی می‌کند:
🧩 Instance methods
🧩 Instance properties
⚡️ Static methods
⚡️ Static properties
💡 Extension Properties
🌟 Extension Properties

ویژگی‌های Extension Properties باعث می‌شن کد شما خواناتر و گویا‌تر بشه.
به جای فراخوانی متدها، حالا می‌تونید از propertyهایی استفاده کنید که طبیعی‌تر به‌نظر میان.

برای مثال، وقتی با کالکشن‌ها کار می‌کنید، معمولاً بررسی می‌کنید که آیا خالی هستند یا نه.
به جای نوشتن مکرر ‎ !items.Any()‎، می‌تونید یک property به نام ‎IsEmpty‎ بسازید: 👇
public static class CollectionExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();

public bool HasItems => source.Any();

public int Count => source.Count();
}
}

public void ProcessOrders(IEnumerable<Order> orders)
{
if (orders.IsEmpty)
{
Console.WriteLine("No orders to process");
return;
}

foreach (var order in orders)
{
// Process order
}
}

💬 در این مثال، حالا می‌تونید به‌صورت طبیعی‌تر بنویسید:
if (orders.IsEmpty)
به‌جای
if (!orders.Any())
نتیجه: کد ساده‌تر، تمیزتر و خواناتر.

🧠 فیلدهای خصوصی و Caching

درون یک بلوک ‎extension‎ می‌تونید فیلدها و متدهای خصوصی تعریف کنید درست مثل کلاس‌های معمولی.
این قابلیت زمانی مفیده که نیاز دارید محاسبات سنگین (مثل ‎ToList()‎) فقط یک‌بار انجام بشن و نتیجه‌ی اون cache بشه.

مثلاً: 👇
public static class CollectionExtensions
{
extension<T>(IEnumerable<T> source)
{
private List<T>? _materializedList;

public List<T> MaterializedList => _materializedList ??= source.ToList();

public bool IsEmpty => MaterializedList.Count == 0;

public T FirstItem => MaterializedList[0];
}
}

در اینجا فیلد خصوصی ‎_materializedList‎ تضمین می‌کنه که ‎ToList()‎ فقط یک‌بار اجرا بشه،
صرف‌نظر از این‌که چند بار به propertyهای مختلف (مثل ‎IsEmpty‎ یا ‎FirstItem‎) دسترسی داشته باشید.

به این ترتیب، performance به‌شکل محسوسی بهبود پیدا می‌کنه. ⚡️
⚙️ Static Extension Members

قابلیت Static Extensions به شما اجازه می‌دهد که متدهای کارخانه‌ای (Factory Methods) یا توابع کمکی (Utility Functions) را به‌صورت استاتیک به یک نوع (Type) اضافه کنید، نه به instance آن.

برای ایجاد ‎static extensions‎، از ‎extension‎ استفاده کنید بدون اینکه پارامتر گیرنده (Receiver Parameter) را نام‌گذاری کنید: 👇
public static class ProductExtensions
{
extension(Product)
{
public static Product CreateDefault() =>
new Product
{
Name = "Unnamed Product",
Price = 0,
StockQuantity = 0,
Category = "Uncategorized",
CreatedDate = DateTime.UtcNow
};

public static bool IsValidPrice(decimal price) =>
price >= 0 && price <= 1000000;

public static string DefaultCategory => "General";
}
}

حالا می‌تونید این اعضای استاتیک رو مستقیماً روی خود نوع (Type) فراخوانی کنید:
var product = Product.CreateDefault();
if (Product.IsValidPrice(999.99m))
{
product.Price = 999.99m;
}

این ویژگی باعث می‌شه که کلاس‌ها تمیزتر و سازمان‌یافته‌تر باشن، مخصوصاً برای ایجاد factory یا helper methods.

🤖 Null-Conditional Assignment

در ‎C# 14‎، عملگرهای Null-Conditional
یعنی ‎?.‎ و ‎?[ ]‎ حالا می‌تونن برای عمل انتساب (assignment) هم استفاده بشن، نه فقط برای دسترسی به اعضا.

🔸 در نسخه‌های قبلی #C، باید قبل از انتساب، به‌صورت دستی null-check انجام می‌دادید:
if (user is not null)
{
user.Profile = LoadProfile();
}

اما حالا می‌تونید همون کار رو خیلی تمیزتر بنویسید: 👇
user?.Profile = LoadProfile();

🧠 این نحو جدید، هم کوتاه‌تره و هم با نحو ‎null-conditional‎ در خواندن مقدارها (reading values) سازگاری بیشتری داره.

🧩 The field Keyword

کلمه‌ی کلیدی ‎field‎ در ‎C# 14‎ یکی از ویژگی‌های کاربردی جدیده که نیاز به تعریف فیلدهای پشتیبان (backing fields) دستی رو در بسیاری از سناریوهای معمولی حذف می‌کنه.

🔹 قبلاً باید فیلد خصوصی رو خودتون تعریف می‌کردید:
public class Record
{
private string _msg;

public string Message
{
get => _msg;
set => _msg = value ?? throw new ArgumentNullException(nameof(value));
}
}

اما حالا با ‎field‎ می‌تونید مستقیماً به فیلد تولید‌شده توسط کامپایلر اشاره کنید:
public class Record
{
public string Message
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
}

💡 می‌تونید از ‎field‎ در هر access modifier مثل ‎get‎، ‎set‎ یا ‎init‎ استفاده کنید.

اگر قبلاً در کدتون متغیری به نام ‎field‎ داشتید، می‌تونید برای جلوگیری از تداخل از ‎@field‎ یا ‎this.field‎ استفاده کنید.

⚡️ کاربرد عملی: Lazy Initialization و مقدارهای پیش‌فرض

کلمه‌ی ‎field‎ برای lazy initialization یا تنظیم مقدارهای پیش‌فرض عالیه: 👇
public class ConfigReader
{
public string FilePath
{
get => field ??= "data/config.json";
set => field = value;
}

public Dictionary<string, string> ConfigValues
{
get => field ??= new Dictionary<string, string>();
set => field = value;
}
}

🧠 به این ترتیب، نیازی نیست فیلدهای پشتیبان جداگانه برای ‎FilePath‎ و ‎ConfigValues‎ تعریف کنید همه‌چیز تمیزتر و خلاصه‌تر می‌شه.

🪄 Lambda Parameters with Modifiers

در ‎C# 14‎ حالا می‌تونید از modifierهایی مثل ‎out‎، ‎ref‎، ‎in‎، ‎scoped‎ و ‎ref readonly‎ در پارامترهای ‎lambda‎ استفاده کنید، بدون اینکه مجبور باشید نوع (type) رو به‌صورت کامل مشخص کنید.

🔸 در نسخه‌های قبلی، باید نوع کامل پارامترها رو می‌نوشتید:
delegate bool TryParse<T>(string text, out T result);

TryParse<int> parse = (string text, out int result) => Int32.TryParse(text, out result);

اما حالا، کامپایلر نوع‌ها رو خودش استنتاج (infer) می‌کنه و کد شما خلاصه‌تر می‌شه: 👇
delegate bool TryParse<T>(string text, out T result);

TryParse<int> parse = (text, out result) => Int32.TryParse(text, out result);

🔹 این ویژگی، هم کد رو مختصرتر می‌کنه، هم type-safety حفظ می‌شه.
نتیجه؟ نوشتن delegateها و lambdaهای پیچیده بسیار ساده‌تر از قبل خواهد بود. 🚀
🧱 Partial Constructors و Events

در ‎C# 14‎ پشتیبانی از Partial Members گسترش یافته و حالا شامل Constructors و Events هم می‌شود.
این قابلیت به‌ویژه برای Source Generator‌ها بسیار مفید است ⚙️

🏗 Partial Constructors

یک ‎Partial Constructor‎ باید شامل یک بخش تعریف‌کننده (Defining Declaration) و یک بخش پیاده‌ساز (Implementing Declaration) باشد.

🔹 مثال:
public partial class User
{
// این بخش تعریف‌کننده است
public partial User(string name);
}

public partial class User
{
// این بخش پیاده‌ساز است
public partial User(string name)
: this() // فراخوانی یک سازنده دیگر در همان کلاس
{
Name = name;
}

public User() { }

public string Name { get; set; }
}


📘 قوانین مربوط به Partial Constructor‌ها:

فقط بخش پیاده‌ساز (implementing part) می‌تواند از ‎this()‎ یا ‎base()‎ برای فراخوانی یک constructor دیگر استفاده کند.

فقط یکی از بخش‌های کلاس می‌تواند از Primary Constructor استفاده کند.

🔧 این ویژگی در پروژه‌هایی که به صورت خودکار کد تولید می‌کنند (مانند source generators)، انعطاف زیادی ایجاد می‌کند و اجازه می‌دهد بخشی از logic در runtime تولید یا تزریق شود.

⚡️ Partial Events

همچنین، ‎Partial Events‎ نیز نیاز به دو بخش دارند — یکی تعریف‌کننده (Defining Declaration) و دیگری پیاده‌ساز (Implementing Declaration).

🔹 مثال:
public partial class Downloader
{
public partial event Action<string> DownloadCompleted;
}

public partial class Downloader
{
public partial event Action<string> DownloadCompleted
{
add { }
remove { }
}
}

📘 در اینجا بخش پیاده‌ساز باید شامل هر دو accessor یعنی ‎add‎ و ‎remove‎ باشد.

این قابلیت به ویژه در سناریوهایی مفید است که بخشی از تعریف event توسط ابزار یا generator تولید می‌شود و بخش دیگر توسط توسعه‌دهنده پیاده‌سازی می‌گردد.

🔗 برای مشاهده‌ی فهرست کامل ویژگی‌های جدید C# 14، می‌توانید به مستندات رسمی Microsoft مراجعه کنید.
🚀 جدیدترین قابلیت‌ها در ASP.NET Core نسخه‌ی ‎.NET 10‎


Validation Support در Minimal APIs
در ‎ASP.NET Core 10‎ پشتیبانی داخلی از Validation برای Minimal APIها اضافه شده است.
این ویژگی داده‌هایی که به endpointهای شما ارسال می‌شوند — شامل query parameters، headers و request bodies را به‌صورت خودکار اعتبارسنجی می‌کند.

برای فعال‌سازی، کافی است سرویس validation را ثبت کنید:
builder.Services.AddValidation();

سیستم validation به‌صورت خودکار نوع‌هایی را که در handlerهای Minimal API شما استفاده شده‌اند شناسایی می‌کند و با استفاده از attributeهای موجود در فضای نام ‎System.ComponentModel.DataAnnotations‎ آن‌ها را اعتبارسنجی می‌کند.

می‌توانید attributeهای اعتبارسنجی را مستقیماً روی پارامترهای endpoint اعمال کنید:
app.MapPost("/products",
([Range(1, int.MaxValue)] int productId, [Required] string name) =>
{
return TypedResults.Ok(new { productId, name });
});

نوع‌های record نیز با validation کار می‌کنند:
public record Product(
[Required] string Name,
[Range(1, 1000)] int Quantity);

app.MapPost("/products", (Product product) =>
{
return TypedResults.Ok(product);
});

وقتی اعتبارسنجی شکست بخورد، runtime به‌صورت خودکار پاسخ 400 Bad Request را همراه با جزئیات خطاهای validation بازمی‌گرداند.

می‌توانید برای endpointهای خاص، validation را غیرفعال کنید:
app.MapPost("/products", (int productId, string name) =>
TypedResults.Ok(productId))
.DisableValidation();

برای شخصی‌سازی پاسخ‌های خطا، می‌توانید رابط ‎IProblemDetailsService‎ را پیاده‌سازی کرده و در Dependency Injection Container ثبت کنید.
این کار امکان تولید پیام‌های خطای یکپارچه و کاربرپسند در کل برنامه را فراهم می‌کند.

🧩 JSON Patch Support در Minimal APIs

برای فعال‌سازی پشتیبانی از JSON Patch با ‎System.Text.Json‎، بسته‌ی زیر را نصب کنید:
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease


🔄 Server-Sent Events (SSE)

Server-Sent Events (SSE)
روشی سبک و قابل‌اعتماد برای ارسال مداوم داده‌ها از سمت سرور به کلاینت‌ها است — بدون پیچیدگی‌های پروتکل‌های دوطرفه مانند WebSocket.
در مدل SSE، سرور از طریق یک اتصال HTTP واحد، داده‌های بلادرنگ را به مرورگر ارسال می‌کند. برخلاف مدل request-response سنتی، نیازی نیست کلاینت‌ها مدام از سرور درخواست بفرستند.

برای ارسال SSE، باید یک جریان داده از نوع ‎IAsyncEnumerable<T>‎ فراهم کنید.

🔹 مثال سرویسی برای تولید داده‌های قیمت سهام:
public record StockPriceEvent(string Id, string Symbol, decimal Price, DateTime Timestamp);

public class StockService
{
public async IAsyncEnumerable<StockPriceEvent> GenerateStockPrices(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var symbols = new[] { "MSFT", "AAPL", "GOOG", "AMZN" };

while (!cancellationToken.IsCancellationRequested)
{
var symbol = symbols[Random.Shared.Next(symbols.Length)];
var price = Math.Round((decimal)(100 + Random.Shared.NextDouble() * 50), 2);
var id = DateTime.UtcNow.ToString("o");

yield return new StockPriceEvent(id, symbol, price, DateTime.UtcNow);

await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
}
}
}

حالا با استفاده از ‎TypedResults.ServerSentEvents‎ یک endpoint برای استریم داده می‌سازیم:
builder.Services.AddSingleton<StockService>();

app.MapGet("/stocks", (StockService stockService, CancellationToken ct) =>
{
return TypedResults.ServerSentEvents(
stockService.GenerateStockPrices(ct),
eventType: "stockUpdate"
);
});

کلاینت‌ها می‌توانند به این endpoint متصل شوند و به‌صورت بلادرنگ بروزرسانی‌های قیمت را دریافت کنند.

برای راهنمایی کامل‌تر درباره‌ی SSE در ‎ASP.NET Core‎، می‌توانید راهنمای رسمی Microsoft را مطالعه کنید.
📘 OpenAPI 3.1 Support

در ‎ASP.NET Core 10‎ پشتیبانی از تولید مستندات ‎OpenAPI 3.1‎ اضافه شده است.
اگرچه شماره نسخه ممکن است کوچک به نظر برسد، اما ‎OpenAPI 3.1‎ تغییرات مهمی دارد، از جمله پشتیبانی کامل از JSON Schema draft 2020-12.

🔹 تغییرات کلیدی در OpenAPI 3.1:
نوع‌های nullable به‌جای ‎nullable: true‎ از ‎type: [<type>, "null"]‎ استفاده می‌کنند.
نوع‌های عددی مانند ‎int‎ و ‎long‎ ممکن است بدون ‎type: integer‎ و با فیلد ‎pattern‎ ظاهر شوند (بسته به تنظیمات serialization).

نسخه‌ی پیش‌فرض OpenAPI اکنون ‎3.1‎ است.

برای تنظیم دستی نسخه‌ی OpenAPI:
builder.Services.AddOpenApi(options =>
{
options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
});


📄 YAML Format Support

اکنون ‎ASP.NET Core‎ می‌تواند مستندات OpenAPI را در قالب YAML نیز ارائه دهد.
YAML خواناتر از JSON است و از رشته‌های چندخطی پشتیبانی می‌کند — مناسب برای مستندات طولانی.

برای فعال‌سازی:
if (app.Environment.IsDevelopment())
{
app.MapOpenApi("/openapi/{documentName}.yaml");
}


🧠 What's New in Blazor

در ‎.NET 10‎، بلیزر پیشرفت‌های زیادی داشته است: Hot Reload برای Blazor WebAssembly و ‎.NET on WebAssembly‎

⚙️ پیکربندی محیط (Environment Configuration) در Blazor WebAssembly مستقل

📊 پروفایلینگ و شمارنده‌های تشخیصی (Performance Profiling & Diagnostic Counters)

پارامتر جدید NotFoundPage برای router

⚡️ پیش‌لود دارایی‌های استاتیک (Static Asset Preloading) در Blazor Web Apps

بهبود در Form Validation
🚀 ویژگی‌های جدید EF Core 10 در 10 NET.

🧩 Complex Types (انواع پیچیده)

در EF Core 10 می‌توان داده‌هایی را مدل کرد که به یک Entity تعلق دارند اما هویت مستقل ندارند.
در حالی که Entityها به جدول‌های جداگانه در دیتابیس نگاشت می‌شوند، Complex Typeها می‌توانند به ستون‌هایی در همان جدول (Table Splitting) یا به یک ستون JSON نگاشت شوند.

این قابلیت امکان مدل‌سازی به سبک Document-Based را فراهم می‌کند و باعث کاهش JOINها و بهبود عملکرد دیتابیس می‌شود.

📘 مثال Table Splitting:
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(c => c.ShippingAddress);
b.ComplexProperty(c => c.BillingAddress);
});


Optional Complex Types (انواع پیچیده اختیاری)

EF Core 10
از Complex Typeهای اختیاری نیز پشتیبانی می‌کند:
public class Customer
{
public int Id { get; set; }
public Address ShippingAddress { get; set; }
public Address? BillingAddress { get; set; }
}


🗃 JSON Mapping (نگاشت به JSON)

اکنون می‌توانید Complex Typeها را به ستون‌های JSON در دیتابیس نگاشت دهید:
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(c => c.ShippingAddress, c => c.ToJson());
b.ComplexProperty(c => c.BillingAddress, c => c.ToJson());
});


💪 Struct Support (پشتیبانی از Struct)

Complex Type
ها حالا می‌توانند از Structها نیز استفاده کنند:
public struct Address
{
public required string Street { get; set; }
public required string City { get; set; }
public required string ZipCode { get; set; }
}


🔗 LeftJoin و RightJoin Operators

در نسخه‌های قبلی EF Core نوشتن LEFT JOIN بسیار پیچیده بود و نیاز به ترکیب SelectMany, GroupJoin, و DefaultIfEmpty داشت.
در .NET 10 حالا می‌توانید با متدهای LeftJoin و RightJoin، کوئری‌های تمیزتر و ساده‌تری بنویسید 👇
var query = context.Students
.LeftJoin(
context.Departments,
student => student.DepartmentID,
department => department.Id,
(student, department) => new { student.Name, Department = department?.Name }
);

🔍 این ویژگی‌ها EF Core را به سمت یک ORM مدرن‌تر، قدرتمندتر و سازگار با معماری‌های Document-oriented و Microservice سوق می‌دهد.
Nobody cares about your career progression more than you do.

Not your boss. Not your partner. Not your mom, dad, cat, dog, or fish.

You can be set up with an amazing manager who wants you to succeed and tries their best to help you grow.

But at the end of the day, YOU are in the driver's seat.
🚀 کاوش File-based Apps در #C در 10 NET.

سی‌شارپ همیشه یک‌جورهایی پُر از تشریفات و ساختارهای اضافی بوده، و خودمان هم این را خوب می‌دانیم. حتی ساده‌ترین برنامهٔ "Hello World" معمولاً به یک فایل راه‌حل (solution)، یک فایل پروژه (csproj)، و آن‌قدر کد boilerplate نیاز داشت که از خودتان می‌پرسیدید شاید بهتر بود از یک زبان اسکریپتی استفاده می‌کردید!

خب، مایکروسافت بالاخره صدایمان را شنید. با NET 10، File-based Apps. معرفی شده‌اند. و واقعاً وقتش بود.

📌 File-based Apps چیستند؟

ایده بسیار ساده است:
کد #C خود را در یک فایل تکی cs. بنویسید و مستقیم اجرا کنید. همین.

نیازی نیست برای یک اسکریپت کوچک که می‌خواهید چند فایل CSV را پردازش کنید یا یک API endpoint را تست کنید، یک ساختار کامل پروژه بسازید.

در این حالت همچنان همهٔ چیزهایی که #C را فوق‌العاده می‌کنند حفظ می‌کنید:
🔸️type safety
🔸️performance

کتابخانه‌های قدرتمند استاندارد💪🏻

اما حالا می‌توانید از #C برای اسکریپت‌های موقتی استفاده کنید؛ جاهایی که ساختن یک پروژهٔ کامل تبدیل به یک دردسر اضافه می‌شود.

و بله:

• می‌توانید NuGet package اضافه کنید
• می‌توانید پروژه‌های دیگر #C را reference کنید
• می‌توانید SDK خاصی را هدف قرار دهید
• و تنظیمات پروژه را فقط با یک فایل انجام دهید

همهٔ این کارها از طریق directive‌هایی که با :# شروع می‌شوند.

این قابلیت روی قابلیت top-level statements که در C# 9 معرفی شد ساخته شده. اگر قرار است اجازه دهیم مردم کلاس و متد Main را حذف کنند، خب چرا اجازه ندهیم کل فایل پروژه را هم حذف کنند؟

🚀 شروع کار با File-based Apps

فرض کنید می‌خواهید سریع بررسی کنید یک تاریخ مشخص مربوط به چه روزی از هفته است. کافی است یک فایل با نام date-checker.cs بسازید:
var targetDate = new DateTime(2025, 12, 31);
Console.WriteLine($"New Year's 2025 falls on a {targetDate.DayOfWeek}");
Console.WriteLine($"That's {(targetDate - DateTime.Today).Days} days from now");

و اجرا کنید:
dotnet run date-checker.cs

اولین باری که این دستور را اجرا می‌کنید، CLI یک مقدار جادوی پشت‌صحنه انجام می‌دهد:

🔹️یک پروژهٔ مجازی می‌سازد
🔹️کد را کامپایل می‌کند
🔹️همه‌چیز را cache می‌کند
🔹️و دفعات بعدی اجرای برنامه تقریباً لحظه‌ای است، چون به‌قدری هوشمند است که تشخیص دهد چیزی تغییر کرده یا نه.

📌مثال واقعی: پردازش سریع داده‌ها

در اینجا است که قضیه جذاب می‌شود. فرض کنیم لازم باشد خیلی سریع مقداری داده‌ی JSON را پردازش کنید و یک گزارش تولید کنید.

بیایید با System.Text.Json و CsvHelper یک کار عملی انجام بدهیم:
#:package CsvHelper@33.1.0
using System.Text.Json;
using CsvHelper;
using System.Globalization;

var json = await File.ReadAllTextAsync("sales_data.json");
var sales = JsonSerializer.Deserialize<List<SaleRecord>>(json);

var topProducts = sales
    .GroupBy(s => s.Product)
    .Select(g => new {
        Product = g.Key,
        TotalRevenue = g.Sum(s => s.Amount),
        UnitsSold = g.Count()
    })
    .OrderByDescending(p => p.TotalRevenue)
    .Take(10);

using var writer = new StreamWriter("top_products.csv");
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteRecords(topProducts);

Console.WriteLine("Report generated! Check top_products.csv");

record SaleRecord(string Product, decimal Amount, DateTime Date);

توجه کنید که در اینجا داریم package reference، عملیات async، LINQ و record type‌ها را با هم ترکیب می‌کنیم—همه در یک فایل واحد که کاملاً شبیه یک اسکریپت منسجم عمل می‌کند. این همان کاری است که معمولاً برای انجامش سراغ Python می‌رفتید، اما حالا می‌توانید در دنیای #C بمانید.