در ماژول Users، میتوانیم بهراحتی GetUserPermissionsConsumer را پیادهسازی کنیم.
این کلاس در صورت یافتن permissions با PermissionsResponse پاسخ میدهد و در صورت خطا با Error پاسخ خواهد داد.
با پذیرش الگوهای پیامرسانی با MassTransit، شما بر روی یک پایهی بسیار مقاومتر ساختار میدهید.
سرویسهای NET. شما اکنون کمتر tightly coupled هستند و این انعطاف را دارید که آنها را بهصورت مستقل توسعه دهید و در برابر خطاهای شبکه یا service outageهای ناگهانی مقاومتر باشید.
الگوی request-response ابزاری قدرتمند در مجموعه ابزارهای پیامرسانی شماست. MassTransit پیادهسازی آن را بهطرز قابلتوجهی ساده میکند و اطمینان میدهد که درخواستها و پاسخها بهطور قابلاعتماد منتقل میشوند.
ما میتوانیم از request-response برای پیادهسازی ارتباط بین ماژولها در یک modular monolith استفاده کنیم.
با این حال، در استفاده از آن زیادهروی نکنید، زیرا ممکن است سیستم شما با latency بیشتر مواجه شود.
کوچک شروع کنید، آزمایش کنید، و ببینید که چطور قابلیت اطمینان و انعطافپذیری پیامرسانی میتواند تجربهی توسعهی شما را متحول کند.
عالی بمونید! 🚀
این کلاس در صورت یافتن permissions با PermissionsResponse پاسخ میدهد و در صورت خطا با Error پاسخ خواهد داد.
public sealed class GetUserPermissionsConsumer(
IPermissionService permissionService)
: IConsumer<GetUserPermissions>
{
public async Task Consume(ConsumeContext<GetUserPermissions> context)
{
Result<PermissionsResponse> result =
await permissionService.GetUserPermissionsAsync(
context.Message.IdentityId);
if (result.IsSuccess)
{
await context.RespondAsync(result.Value);
}
else
{
await context.RespondAsync(result.Error);
}
}
}
💭 نتیجهگیری
با پذیرش الگوهای پیامرسانی با MassTransit، شما بر روی یک پایهی بسیار مقاومتر ساختار میدهید.
سرویسهای NET. شما اکنون کمتر tightly coupled هستند و این انعطاف را دارید که آنها را بهصورت مستقل توسعه دهید و در برابر خطاهای شبکه یا service outageهای ناگهانی مقاومتر باشید.
الگوی request-response ابزاری قدرتمند در مجموعه ابزارهای پیامرسانی شماست. MassTransit پیادهسازی آن را بهطرز قابلتوجهی ساده میکند و اطمینان میدهد که درخواستها و پاسخها بهطور قابلاعتماد منتقل میشوند.
ما میتوانیم از request-response برای پیادهسازی ارتباط بین ماژولها در یک modular monolith استفاده کنیم.
با این حال، در استفاده از آن زیادهروی نکنید، زیرا ممکن است سیستم شما با latency بیشتر مواجه شود.
کوچک شروع کنید، آزمایش کنید، و ببینید که چطور قابلیت اطمینان و انعطافپذیری پیامرسانی میتواند تجربهی توسعهی شما را متحول کند.
عالی بمونید! 🚀
🔖هشتگها:
#MassTransit #RequestResponse #MessagingPattern
📌معرفی YARP
YARP (Yet Another Reverse Proxy)
یک کتابخانه reverse proxy بسیار قابل تنظیم برای NET. است. این کتابخانه طراحی شده تا یک چارچوب پروکسی قابل اعتماد، منعطف، مقیاسپذیر، امن و آسان برای استفاده ارائه دهد. YARP به توسعهدهندگان کمک میکند تا راهکارهای reverse proxy قدرتمند و بهینه متناسب با نیازهای خاص خود ایجاد کنند. 🚀
کارکرد یک Reverse Proxy
یک reverse proxy سروری است که بین دستگاههای مشتری و سرورهای بکاند قرار میگیرد. این سرور درخواستهای مشتری را به سرور مناسب منتقل میکند و سپس پاسخ سرور را به مشتری برمیگرداند. Reverse proxy چندین مزیت دارد:
🔹️Routing:
هدایت درخواستها به سرورهای مختلف بکاند بر اساس قوانین از پیش تعریف شده، مانند الگوهای URL یا هدرهای درخواست. برای مثال، درخواستهای /images، /api و /db میتوانند به سرورهای image، api و database هدایت شوند.
🔹️Load Balancing:
توزیع ترافیک ورودی بین چند سرور بکاند برای جلوگیری از بارگذاری بیش از حد یک سرور خاص. این توزیع باعث افزایش عملکرد و قابلیت اطمینان میشود.
🔹️Scalability:
با توزیع ترافیک بین چند سرور، reverse proxy به برنامه کمک میکند تا بتواند کاربران و بار بیشتر را مدیریت کند. سرورهای بکاند میتوانند بدون تأثیر بر مشتری اضافه یا حذف شوند.
🔹️SSL/TLS Termination:
بار رمزنگاری و رمزگشایی TLS را از سرورهای بکاند برداشته و بار آنها را کاهش میدهد.
🔹️Connection abstraction, decoupling and control over URL-space:
درخواستهای ورودی از مشتری و پاسخهای خروجی از بکاند مستقل هستند. این استقلال امکان:
• استفاده از نسخههای مختلف HTTP (HTTP/1.1، HTTP/2، HTTP/3) و ارتقا یا کاهش نسخهها
• مدیریت طول عمر اتصالها، مانند نگه داشتن اتصال طولانی در بکاند در حالی که اتصالات کوتاه مشتری حفظ میشوند
• کنترل URL: URLهای ورودی میتوانند قبل از ارسال به بکاند تغییر داده شوند، و نقشه داخلی خدمات میتواند بدون تأثیر بر URL خارجی تغییر کند
🔹️Security:
نقاط انتهایی داخلی میتوانند از دید خارجی مخفی بمانند و از برخی حملات سایبری مانند DDoS محافظت کنند 🔒
🔹️Caching:
منابع پر درخواست میتوانند کش شوند تا بار روی سرورهای بکاند کاهش یابد و زمان پاسخ بهبود یابد
🔹️Versioning:
نسخههای مختلف یک API با استفاده از نگاشتهای مختلف URL پشتیبانی میشوند
🔹️Simplified maintenance:
Reverse proxies
میتوانند SSL/TLS Termination و وظایف دیگر را مدیریت کنند، که پیکربندی و نگهداری سرورهای بکاند را ساده میکند. برای مثال، گواهینامهها و سیاستهای امنیتی میتوانند در سطح پروکسی مدیریت شوند به جای هر سرور جداگانه 🛠
نحوه مدیریت HTTP توسط Reverse Proxy
یک reverse proxy درخواستها و پاسخهای HTTP را به این صورت مدیریت میکند:
🔸️دریافت درخواستها: پروکسی روی پورتها و endpoints مشخص به درخواستهای HTTP مشتریان گوش میدهد.
🔸️Terminating Connections:
اتصالات HTTP ورودی در پروکسی خاتمه مییابند و اتصالات جدید برای درخواستهای خروجی ایجاد میشوند.
🔸️Routing requests:
بر اساس قوانین و تنظیمات از پیش تعریف شده، پروکسی تعیین میکند که کدام سرور بکاند یا خوشهای از سرورها باید درخواست را پردازش کنند.
🔸️Forwarding requests:
پروکسی درخواست مشتری را به سرور مناسب ارسال میکند و مسیر و هدرها را در صورت نیاز تغییر میدهد.
🔸️Connection pooling:
اتصالات خروجی به صورت pooled مدیریت میشوند تا بار اتصال کاهش یابد و از مزایای HTTP 1.1 و درخواستهای موازی HTTP/2 و HTTP/3 استفاده شود
🔸️Processing responses:
سرور بکاند درخواست را پردازش کرده و پاسخ را به پروکسی ارسال میکند
🔸️Returning responses:
پروکسی پاسخ را از سرور دریافت و به مشتری برمیگرداند و در صورت نیاز تغییرات لازم روی پاسخ اعمال میکند ✅
این روند اطمینان میدهد که مشتری با پروکسی تعامل دارد نه مستقیماً با سرورهای بکاند، و مزایای load balancing، امنیت، versioning و غیره را فراهم میکند.
چرا 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
🧩 الگوی 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 حسابها به این میزان از اطمینان نیاز است. 💳
این تصمیم بستگی به سطح اطمینان مورد نیاز دارد:
اگر تکرار عملیات عواقب واقعی دارد (مالی یا دادهای)، باید idempotency را صریحاً اعمال کنید.
در غیر این صورت، retry کردن عملیات ممکن است قابلقبول باشد.
همهی consumerها به سربار بررسیهای idempotency نیاز ندارند.
اگر عملیات شما بهطور طبیعی idempotent است، میتوانید از جدول اضافه و تراکنش صرفنظر کنید.
بهعنوان مثال:
بهروزرسانی projectionها 📊
تنظیم flag وضعیت ✅
یا refresh کردن cache 🧩
همگی نمونههایی از عملیات deterministic هستند که اجرای چندبارهی آنها خطری ندارد.
مثل:
«تنظیم وضعیت کاربر روی Active» یا «بازسازی Read Model» — اینها state را بازنویسی میکنند نه اینکه چیزی به آن اضافه کنند.
برخی از handlerها هم از Precondition Check برای جلوگیری از تکرار استفاده میکنند.
اگر handler در حال بهروزرسانی یک entity باشد، ابتدا میتواند بررسی کند که آیا entity در وضعیت مورد نظر هست یا نه؛ اگر هست، بهسادگی return کند.
این محافظ ساده در بسیاری موارد کافی است.
⚠️ الگوی Idempotent Consumer را بدون فکر در همهجا اعمال نکنید.
فقط در جایی از آن استفاده کنید که از آسیب واقعی (مالی یا ناسازگاری دادهای) جلوگیری کند.
برای سایر موارد، سادگی بهتر است.
سیستمهای توزیعشده ذاتاً غیرقابلپیشبینی هستند. ⚙️ 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ها را تحمل کند.
در این صورت، سیستم توزیعشدهات بسیار قابلاعتمادتر خواهد شد. 🔐
نکتهی جالب اینجاست که وقتی این اصل را واقعاً درک میکنی، آن را در همهی سیستمهای واقعی دنیا میبینی. 🌍
امیدوارم این مطلب برات مفید بوده باشه 💙
⚖️ 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 با استفاده از 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
🚀 چه چیزهایی در 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