C# Geeks (.NET) – Telegram
🔬 کالبدشکافی آبجکت‌ها با Deconstructors در #C


تو پست قبلی دیدیم چطور با Object Initializers یه آبجکت رو به صورت شیک "بسازیم". حالا بیاید ببینیم چطور می‌تونیم یه آبجکت رو به همون زیبایی "بشکافیم" و اجزاش رو استخراج کنیم!

سی‌شارپ یه قابلیت مدرن و قدرتمند به اسم Deconstructor داره که دقیقاً برعکس سازنده (Constructor) عمل می‌کنه.

1️⃣ حالا Deconstructor چیست؟
این Deconstructor یه متد خاص به اسم Deconstruct هست که شما تو کلاستون تعریف می‌کنید. این متد، فیلدها و پراپرتی‌های کلاس شما رو به مجموعه‌ای از متغیرهای خروجی (out parameters) تبدیل می‌کنه.
public class Rectangle
{
public readonly float Width, Height;

public Rectangle(float width, float height)
{
Width = width;
Height = height;
}

// این متد Deconstructor ماست
public void Deconstruct(out float width, out float height)
{
width = Width;
height = Height;
}
}


2️⃣ جادوی سینتکس: کالبدشکافی در یک خط!
حالا که متد Deconstruct رو داریم، #C به ما یه سینتکس فوق‌العاده شیک و خوانا برای استفاده ازش میده:
var rect = new Rectangle(3, 4);

// جادو اینجا اتفاق میفته!
// این خط، متد Deconstruct رو صدا میزنه
(float width, float height) = rect;

Console.WriteLine($"Width: {width}, Height: {height}"); // خروجی: Width: 3, Height: 4
این سینتکس، کد شما رو به شدت تمیز و بیانگر می‌کنه.


3️⃣ ترفندهای خلاصه‌نویسی
این قابلیت چند تا ترفند برای خلاصه‌تر شدن هم داره:

استفاده از var:
// کامپایلر خودش نوع‌ها رو تشخیص میده
var (width, height) = rect;

نادیده گرفتن با _ (Discard):
اگه فقط به یکی از مقادیر نیاز دارید، می‌تونید اون یکی رو با _ نادیده بگیرید:
// ما اینجا فقط به ارتفاع نیاز داریم
var (_, height) = rect;

تخصیص به متغیرهای موجود:
اگه متغیرها رو از قبل دارید، دیگه نیازی به تعریفشون نیست:
float w, h;
(w, h) = rect;

🤔 حرف حساب و تجربه شما
شما تا حالا از Deconstructors تو کدهاتون استفاده کردید؟ به نظرتون بهترین کاربرد این قابلیت کجاست؟

🔖 هشتگ‌ها:
#CSharp #Programming #Developer #DotNet #ModernCSharp #CleanCode #BestPractices
🏷 فیلترهای کوئری نام‌دار در EF 10 (چندین فیلتر کوئری برای هر انتیتی)


فیلترهای کوئری سراسری (global query filters) در Entity Framework Core از دیرباز راهی مناسب برای اعمال شرایط مشترک به تمام کوئری‌های یک انتیتی بوده‌اند. این فیلترها به ویژه در سناریوهایی مانند حذف منطقی (soft deletion) 🗑 و چند-مستأجری (multi-tenancy) 🏢 مفید هستند، جایی که شما می‌خواهید همان دستور WHERE به صورت خودکار به هر کوئری اضافه شود.

با این حال، نسخه‌های قبلی EF Core از یک محدودیت بزرگ 😩 رنج می‌بردند: هر نوع انتیتی فقط می‌توانست یک فیلتر تعریف شده داشته باشد. اگر نیاز به ترکیب چندین شرط داشتید، یا باید عبارات && صریح می‌نوشتید یا فیلترها را به صورت دستی در کوئری‌های خاص غیرفعال و دوباره اعمال می‌کردید.

با EF 10، این وضعیت تغییر می‌کند.

قابلیت جدید فیلترهای کوئری نام‌دار (named query filters) به شما امکان می‌دهد چندین فیلتر را به یک انتیتی متصل کرده و با نام به آن‌ها ارجاع دهید. سپس می‌توانید فیلترهای فردی را در صورت نیاز غیرفعال کنید، به جای اینکه همه فیلترها را یکجا خاموش کنید.

بیایید این قابلیت جدید، چرایی اهمیت آن و چند روش عملی برای استفاده از آن را بررسی کنیم.

🔎فیلترهای کوئری (Query Filters) چه هستند؟

اگر مدتی است که از EF Core استفاده می‌کنید، ممکن است از قبل با فیلترهای کوئری سراسری آشنا باشید. یک فیلتر کوئری، شرطی است که EF به طور خودکار به تمام کوئری‌ها برای یک نوع انتیتی خاص اعمال می‌کند. در پشت صحنه، EF هر زمان که آن انتیتی کوئری می‌شود، یک دستور WHERE اضافه می‌کند. کاربردهای معمول عبارتند از:

• حذف منطقی: فیلتر کردن ردیف‌هایی که IsDeleted در آن‌ها true است.

• چند-مستأجری: فیلتر کردن بر اساس TenantId تا هر مستأجر فقط داده‌های خود را ببیند.

برای مثال، یک فیلتر حذف منطقی ممکن است اینگونه پیکربندی شود:
modelBuilder.Entity<Order>()
.HasQueryFilter(order => !order.IsDeleted);

با وجود این فیلتر، هر کوئری روی Orders به طور خودکار رکوردهای حذف شده منطقی را حذف می‌کند. برای شامل کردن داده‌های حذف شده، می‌توانید IgnoreQueryFilters() را روی کوئری فراخوانی کنید. عیب این کار این است که تمام فیلترهای روی آن انتیتی غیرفعال می‌شوند.

استفاده از چندین فیلتر کوئری

تاکنون، EF فقط یک فیلتر کوئری برای هر انتیتی مجاز می‌دانست. برای ترکیب فیلترها باید یک عبارت واحد با && می‌نوشتید:
modelBuilder.Entity<Order>()
.HasQueryFilter(order => !order.IsDeleted && order.TenantId == tenantId);

💡این کار می‌کند اما غیرفعال کردن انتخابی یک شرط را غیرممکن می‌سازد. EF 10 یک جایگزین بهتر معرفی می‌کند: فیلترهای کوئری نام‌دار.

برای متصل کردن چندین فیلتر به یک انتیتی، HasQueryFilter را با یک نام برای هر فیلتر فراخوانی کنید:
modelBuilder.Entity<Order>()
.HasQueryFilter("SoftDeletionFilter", order => !order.IsDeleted)
.HasQueryFilter("TenantFilter", order => order.TenantId == tenantId);

اکنون می‌توانید فقط فیلتر حذف منطقی را خاموش کنید در حالی که فیلتر مستأجر را فعال نگه می‌دارید:
// تمام سفارشات (شامل حذف شده‌های منطقی) برای مستأجر فعلی را برمی‌گرداند
var allOrders = await context.Orders
.IgnoreQueryFilters(["SoftDeletionFilter"])
.ToListAsync();


💡 نکته: استفاده از ثابت‌ها برای نام فیلترها
فیلترهای نام‌دار از کلیدهای رشته‌ای استفاده
می‌کنند. هاردکد کردن این نام‌ها می‌تواند باعث ایجاد "رشته‌های جادویی" (magic strings) شکننده شود. برای جلوگیری از این مشکل، ثابت‌ها را برای نام فیلترهای خود تعریف کنید.
public static class OrderFilters
{
public const string SoftDelete = nameof(SoftDelete);
public const string Tenant = nameof(Tenant);
}

modelBuilder.Entity<Order>()
.HasQueryFilter(OrderFilters.SoftDelete, order => !order.IsDeleted)
.HasQueryFilter(OrderFilters.Tenant, order => order.TenantId == tenantId);


یک رویه بهتر دیگر، پیچیدن فراخوانی ignore در یک متد توسعه (extension method) است:
public static IQueryable<Order> IncludeSoftDeleted(this IQueryable<Order> query)
=> query.IgnoreQueryFilters([OrderFilters.SoftDelete]);
جمع‌بندی 📝
معرفی فیلترهای کوئری نام‌دار در EF 10 یکی از محدودیت‌های دیرینه را برطرف می‌کند. شما اکنون می‌توانید:

🔹 چندین فیلتر را به یک انتیتی متصل کرده و آن‌ها را به صورت جداگانه مدیریت کنید.

🔹 فیلترهای خاصی را در یک کوئری LINQ با استفاده از IgnoreQueryFilters(["FilterName"]) به صورت انتخابی غیرفعال کنید.

🔹 الگوهای رایجی مانند حذف منطقی و چند-مستأجری را ساده‌سازی کنید.

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

آن‌ها را در نسخه پیش‌نمایش امتحان کنید و به این فکر کنید که چگونه می‌توانند پایگاه کد شما را ساده‌تر کنند.

🔖 هشتگ‌ها:
#CSharp #Programming #Developer #DotNet #EntityFrameWork #EF
🏢 معماری چند-مستأجره (Multi-Tenancy) با EF Core (قسمت ۱):

یک دیتابیس برای همه
اپلیکیشن‌های مدرن SaaS (نرم‌افزار به عنوان سرویس) یک ویژگی مشترک دارن: چند-مستأجری (Multi-Tenancy).

یعنی یک اپلیکیشن به چندین مشتری (مستأجر یا tenant) سرویس میده، ولی داده‌های هر مشتری کاملاً از بقیه ایزوله هست. امروز در قسمت اول از این سری، با این معماری و روش پیاده‌سازی "یک دیتابیس برای همه" با EF Core آشنا میشیم.

1️⃣ دو رویکرد اصلی برای چند-مستأجری
برای ایزوله کردن داده‌های مشتریان، دو راه اصلی وجود داره:

🧠 ایزوله‌سازی منطقی (Logical Isolation):

یک دیتابیس واحد برای همه مشتریان، که داده‌ها با یک شناسه مثل TenantId از هم جدا میشن. (موضوع این پست)

🏬 ایزوله‌سازی فیزیکی (Physical Isolation):

یک دیتابیس کاملاً مجزا برای هر مشتری.

انتخاب بین این دو، به نیاز پروژه بستگی داره. برای صنایعی مثل حوزه سلامت که به ایزوله‌سازی بالا نیاز دارن، روش دوم الزامیه.

2️⃣ ابزار اصلی ما: EF Core Query Filters 🔍
برای پیاده‌سازی روش "یک دیتابیس برای همه"، ابزار اصلی ما در EF Core، فیلترهای کوئری سراسری (Global Query Filters) هست. این قابلیت به ما اجازه میده یه شرط WHERE رو به صورت خودکار به تمام کوئری‌های یک انتیتی خاص اضافه کنیم. یک بار پیاده‌سازیش می‌کنیم و دیگه تقریباً فراموشش می‌کنیم!

3️⃣ پیاده‌سازی قدم به قدم
برای این کار به دو چیز نیاز داریم: ۱. راهی برای شناسایی مستأجر فعلی. ۲. راهی برای فیلتر کردن داده‌ها برای اون مستأجر.

قدم اول: شناسایی مستأجر با TenantProvider
اول از همه، باید بفهمیم درخواست فعلی برای کدوم مستأجره. ما این منطق رو تو یه کلاس جدا به اسم TenantProvider کپسوله می‌کنیم. این کلاس، TenantId رو از هدر (X-TenantId) درخواست HTTP می‌خونه.
public sealed class TenantProvider
{
private const string TenantIdHeaderName = "X-TenantId";
private readonly IHttpContextAccessor _httpContextAccessor;

public TenantProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}

public string TenantId => _httpContextAccessor
.HttpContext?
.Request
.Headers[TenantIdHeaderName];
}


💡نکته: راه‌های امن‌تر دیگری هم برای گرفتن TenantId وجود داره مثل JWT Claim یا API Key.


قدم دوم: اعمال فیلتر سراسری در DbContext
حالا که می‌تونیم مستأجر فعلی رو پیدا کنیم، باید به EF Core بگیم که تمام کوئری‌ها رو بر اساس اون فیلتر کنه. بهترین جا برای این کار، متد OnModelCreating در DbContext ماست.
public class OrdersDbContext : DbContext
{
private readonly string _tenantId;

public OrdersDbContext(
DbContextOptions<OrdersDbContext> options,
TenantProvider tenantProvider)
: base(options)
{
// TenantId رو از TenantProvider می‌گیریم
_tenantId = tenantProvider.TenantId;
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// این فیلتر به صورت خودکار به تمام کوئری‌های انتیتی Order اضافه میشه
modelBuilder
.Entity<Order>()
.HasQueryFilter(o => o.TenantId == _tenantId);
}
}


حالا هر کوئری‌ای که روی جدول Order زده بشه، EF Core به صورت خودکار شرط WHERE TenantId = 'current_tenant_id' رو بهش اضافه می‌کنه!


🔖 هشتگ‌ها:
#CSharp #ASPNETCore #DotNet #MultiTenancy #EntityFrameworkCore #SoftwareArchitecture #Backend
🏢 معماری چند-مستأجره با EF Core (قسمت ۲):

هر مستأجر، دیتابیس خودش

در قسمت اول، روش ایزوله‌سازی منطقی (یک دیتابیس برای همه) رو با Query Filters پیاده‌سازی کردیم. این روش برای خیلی از پروژه‌ها عالیه.

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

1️⃣ چالش جدید: مدیریت Connection String داینامیک 🔌
در این سناریو، دیگه Query Filters به کارمون نمیاد، چون با دیتابیس‌های مختلفی سر و کار داریم. چالش اصلی ما اینه که به ازای هر درخواست، بتونیم DbContext رو با Connection String مخصوص همون مستأجر پیکربندی کنیم.

2️⃣ پیاده‌سازی قدم به قدم
قدم اول: ذخیره اطلاعات مستأجرها ⚙️
اول از همه، باید اطلاعات هر مستأجر و Connection String دیتابیسش رو یه جا ذخیره کنیم. برای مثال، در فایل appsettings.json:

"Tenants": [
{ "Id": "tenant-1", "ConnectionString": "Host=tenant1.db;Database=db1" },
{ "Id": "tenant-2", "ConnectionString": "Host=tenant2.db;Database=db2" }
]


قدم دوم: آپدیت کردن TenantProvider 🕵️‍♂️حالا TenantProvider رو آپدیت می‌کنیم تا علاوه بر TenantId، بتونه Connection String مربوط به مستأجر فعلی رو هم از تنظیمات بخونه و به ما بده.
public sealed class TenantProvider
{
// ... (Properties for HttpContextAccessor and TenantSettings)

public TenantProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<TenantSettings> tenantsOptions)
{
// ...
_tenantSettings = tenantsOptions.Value;
}

public string TenantId => /* ... gets tenantId from header ... */;

public string GetConnectionString()
{
return _tenantSettings.Tenants
.Single(t => t.Id == TenantId).ConnectionString;
}
}


قدم سوم: جادوی DI (ثبت داینامیک DbContext) این مهم‌ترین و جادویی‌ترین بخش کاره! ما DbContext رو جوری در Program.cs ثبت می‌کنیم که به ازای هر درخواست، اول TenantProvider رو اجرا کنه، Connection String رو بگیره و بعد DbContext رو با اون پیکربندی کنه.
builder.Services.AddDbContext<OrdersDbContext>((serviceProvider, options) =>
{
// TenantProvider رو از DI میگیریم
var tenantProvider = serviceProvider.GetRequiredService<TenantProvider>();

// Connection String مخصوص مستأجر فعلی رو میگیریم
var connectionString = tenantProvider.GetConnectionString();

// و DbContext رو با اون کانفیگ می‌کنیم
options.UseSqlServer(connectionString);
});


🔐 نکته امنیتی مهم
قرار دادن Connection Stringها به صورت مستقیم در appsettings.json برای محیط پروداکشن امن نیست. همیشه از ابزارهای مدیریت secret مثل Azure Key Vault یا NET User Secrets. برای محیط توسعه استفاده کنید.
🤔 حرف حساب و تجربه شمابا این دو روش

شما الان جعبه ابزار کاملی برای پیاده‌سازی هر نوع معماری چند-مستأجره در ASP.NET Core دارید.

شما تو پروژه‌هاتون با کدوم مدل چند-مستأجری کار کردید؟ تک دیتابیس یا چند دیتابیس؟ چالش‌ها و مزایای هر کدوم از نظر شما چیه؟

🔖 هشتگ‌ها:
#CSharp #ASPNETCore #DotNet #MultiTenancy #EntityFrameworkCore #SoftwareArchitecture #Backend
🔐 ذخیره‌سازی امن اسرار برنامه در محیط توسعه در ASP.NET Core


این مقاله توضیح می‌دهد که چگونه داده‌های حساس را برای یک اپلیکیشن ASP.NET Core روی یک ماشین توسعه مدیریت کنید. هرگز پسوردها یا دیگر داده‌های حساس را در سورس کد یا فایل‌های پیکربندی ذخیره نکنید. اسرار محیط پروداکشن نباید برای توسعه یا تست استفاده شوند. اسرار نباید همراه با اپلیکیشن دیپلوی شوند. اسرار پروداکشن باید از طریق ابزارهای کنترل‌شده مانند Azure Key Vault 🔑 قابل دسترسی باشند.

متغیرهای محیطی (Environment variables) 🌍

متغیرهای محیطی برای جلوگیری از ذخیره‌سازی اسرار برنامه در کد یا فایل‌های کانفیگ محلی استفاده می‌شوند. متغیرهای محیطی، مقادیر پیکربندی را برای تمام منابع پیکربندی که قبلاً مشخص شده‌اند، بازنویسی (override) می‌کنند.

⚠️ هشدار

متغیرهای محیطی معمولاً به صورت متن ساده و رمزنگاری‌نشده ذخیره می‌شوند. اگر ماشین یا فرآیند به خطر بیفتد، متغیرهای محیطی می‌توانند توسط افراد غیرقابل اعتماد قابل دسترسی باشند.

جداکننده : با کلیدهای سلسله‌مراتبی متغیرهای محیطی روی همه پلتفرم‌ها کار نمی‌کند (مثلاً Bash از آن پشتیبانی نمی‌کند). اما آندرلاین دوتایی (__):

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

ابزار Secret Manager 🕵️‍♂️

ابزار Secret Manager داده‌های حساس را در طول توسعه اپلیکیشن ذخیره می‌کند. در این زمینه، یک داده حساس، یک سرّ برنامه (app secret) است. اسرار برنامه در مکانی جدا از درخت پروژه ذخیره می‌شوند و به یک پروژه خاص مرتبط هستند. اسرار برنامه به سورس کنترل چک‌این نمی‌شوند.

⚠️ هشدار

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

نحوه کار ابزار Secret Manager 🪄

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

Windows: 📁 %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json

Linux / macOS: 📁 ~/.microsoft/usersecrets/<user_secrets_id>\secrets.json

کدی ننویسید که به مکان یا فرمت داده‌های ذخیره شده با Secret Manager وابسته باشد.

فعال کردن ذخیره‌سازی اسرار
از طریق CLI:

در پوشه پروژه، دستور زیر را اجرا کنید:

dotnet user-secrets init

این دستور یک عنصر UserSecretsId به فایل csproj. شما اضافه می‌کند.
از طریق ویژوال استودیو: 🖱️
در Solution Explorer، روی پروژه راست‌کلیک کرده و Manage User Secrets را انتخاب کنید.

تنظیم یک سرّ ✍️

یک سرّ از یک کلید و مقدار تشکیل شده است. برای تنظیم آن، از دستور set استفاده کنید:

dotnet user-secrets set "Movies:ServiceApiKey" "12345"

کالن (:) در اینجا نشان می‌دهد که Movies یک آبجکت با پراپرتی ServiceApiKey است.

دسترسی به یک سرّ 📖

1️⃣ ثبت منبع پیکربندی User Secrets:
در اپلیکیشن‌های وب مدرن ASP.NET Core، WebApplication.CreateBuilder(args) به صورت خودکار AddUserSecrets را در محیط Development فراخوانی می‌کند.

2️⃣ خواندن سرّ از طریق Configuration API: ⚙️
شما می‌توانید اسرار را دقیقاً مانند مقادیر appsettings.json از طریق IConfiguration بخوانید.
var builder = WebApplication.CreateBuilder(args);
var movieApiKey = builder.Configuration["Movies:ServiceApiKey"];
var app = builder.Build();

app.MapGet("/", () => movieApiKey);

app.Run();

مثال در یک Page Model:
public class IndexModel : PageModel
{
private readonly IConfiguration _config;

public IndexModel(IConfiguration config)
{
_config = config;
}

public void OnGet()
{
var moviesApiKey = _config["Movies:ServiceApiKey"];
// ...
}
}
مپ کردن اسرار به یک POCO 🔗

مپ کردن یک آبجکت کامل به یک POCO (یک کلاس ساده #C با پراپرتی‌ها) برای جمع‌آوری پراپرتی‌های مرتبط مفید است.فرض کنید فایل secrets.json برنامه شامل دو سرّ زیر است:
{
"Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
"Movies:ServiceApiKey": "12345"
}

کد زیر به یک POCO سفارشی به نام MovieSettings متصل شده و به مقدار پراپرتی ServiceApiKey دسترسی پیدا می‌کند:
var moviesConfig = 
Configuration.GetSection("Movies").Get<MovieSettings>();
var _moviesApiKey = moviesConfig.ServiceApiKey;

اسرار Movies:ConnectionString و Movies:ServiceApiKey به پراپرتی‌های مربوطه در MovieSettings مپ می‌شوند:
public class MovieSettings
{
public string ConnectionString { get; set; }
public string ServiceApiKey { get; set; }
}


جایگزینی رشته با اسرار 🛠️

ذخیره پسوردها به صورت متن ساده ناامن است. پسورد را به عنوان یک سرّ ذخیره کنید و در زمان اجرا آن را به رشته اتصال اضافه کنید.
dotnet user-secrets set "DbPassword" "<secret value>"

using System.Data.SqlClient;

var builder = WebApplication.CreateBuilder(args);

var conStrBuilder = new SqlConnectionStringBuilder(
builder.Configuration.GetConnectionString("Movies"));
conStrBuilder.Password = builder.Configuration["DbPassword"];
var connection = conStrBuilder.ConnectionString;

var app = builder.Build();
app.MapGet("/", () => connection);
app.Run();


📑 دستورات CLI


لیست کردن اسرار list 📋
dotnet user-secrets list

حذف یک سرّ واحد remove 🗑️
dotnet user-secrets remove "Movies:ConnectionString"

حذف تمام اسرار clear ♻️
dotnet user-secrets clear

مدیریت اسرار کاربر با ویژوال استودیو :
برای مدیریت اسرار کاربر در ویژوال استودیو، در Solution Explorer روی پروژه راست‌کلیک کرده و Manage User Secrets را انتخاب کنید.

اسرار کاربر در اپلیکیشن‌های غیر وب 🖥️
برای اپلیک
یشن‌هایی مانند Console App، باید پکیج‌های NuGet 📦 زیر را به صورت دستی اضافه کنید:
با استفاده از PowerShell:

Install-Package Microsoft.Extensions.Configuration
Install-Package Microsoft.Extensions.Configuration.UserSecrets

با استفاده از .NET CLI:
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.UserSecrets

پس از نصب پکیج‌ها، پروژه را مقداردهی اولیه کرده و اسرار را همانند یک اپلیکیشن وب تنظیم کنید. مثال زیر یک اپلیکیشن کنسول را نشان می‌دهد که مقدار یک سرّ را که با کلید "AppSecret" تنظیم شده، بازیابی می‌کند:
using Microsoft.Extensions.Configuration;

class Program
{
static void Main(string[] args)
{
IConfigurationRoot config = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();

Console.WriteLine(config["AppSecret"]);
}
}
🚀 چرا Connection String رو مستقیم تو appsettings.json نگذاریم؟

آشنایی کامل با Azure Key Vault

🔐 وقتی توی یک پروژه ASP.NET Core داری کار می‌کنی، معمولاً Connection String و Credentialهای حساس رو توی appsettings.json میزاری.
اما این کار چند تا مشکل اساسی داره:

📂 فایل پیکربندی معمولاً داخل سورس کنترل (Git) هست → پس هرکسی به مخزن دسترسی داشته باشه، به داده‌های حساس هم دسترسی پیدا می‌کنه.

🛠 حتی اگه تو gitignore. بگذاری، باز هم روی سرور یا محیط‌های مشترک ممکنه نشت کنه.

🔓 توی Production، این اطلاعات ممکنه لاگ بشه یا با خطاها لو بره.

راه‌حل امن و استاندارد: استفاده از Azure Key Vault

🔍 حالا Azure Key Vault چیه؟
یک سرویس ابری از Azure برای ذخیره‌سازی امن:

🔑 Secrets (مثل Connection String، API Keys، Tokenها)

🗝 Keys (کلیدهای رمزنگاری)

📜 Certificates (گواهی‌ها)

💯مزیت‌ها:

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

• کنترل سطح دسترسی به کمک Microsoft Entra ID.

• پشتیبانی از Hardware Security Module (HSM).

📦 پکیج‌های لازم
dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
dotnet add package Azure.Identity

🛠 راه‌اندازی در حالت Development
برای محیط لوکال، از Secret Manager استفاده کن:
<PropertyGroup>
<UserSecretsId>{GUID}</UserSecretsId>
</PropertyGroup>

اضافه کردن Secret:
dotnet user-secrets set "DbConnection" "Server=.;Database=Test;User Id=sa;Password=1234"


🏭 راه‌اندازی در Production با Azure Key Vault
1️⃣ ساخت Resource Group و Key Vault
az group create --name "MyGroup" --location "westeurope"
az keyvault create --name "myvault123" --resource-group "MyGroup" --location "westeurope"

2️⃣ ذخیره Secrets
az keyvault secret set --vault-name "myvault123" --name "DbConnection" --value "Server=sql.example.com;Database=Prod;..."


🗂 اتصال ASP.NET Core به Key Vault
روش ۱: با گواهی X.509
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
new ClientCertificateCredential(
builder.Configuration["AzureADDirectoryId"],
builder.Configuration["AzureADApplicationId"],
x509Certificate));

روش 2️⃣ : با Managed Identity (پیشنهادی در Azure)
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
new DefaultAzureCredential());


📌 بهترین نکات امنیتی

🔒 هر محیط (Dev/Prod) باید Key Vault جدا داشته باشه.

📛 از نام‌گذاری استاندارد برای Secrets استفاده کن (برای بخش‌ها از -- به جای :).

🔄 برای تغییرات حساس، ReloadInterval رو تنظیم کن تا مقادیر به‌روزرسانی بشن.

🛑 و Secrets رو هرگز در لاگ‌ها چاپ نکن.

🎯 نتیجه
با این روش:

• اطلاعات حساس هرگز در سورس‌کد یا فایل‌های config ذخیره نمی‌شن.

• در صورت نشت سورس یا فایل‌ها، اطلاعات Production در امانه.

• مدیریت و تغییر مقادیر حساس از طریق Azure Portal یا CLI انجام میشه، بدون نیاز به Deploy دوباره اپلیکیشن.

🔖 هشتگ‌ها:
#CSharp #ASPNetCore #Azure #KeyVault #CodeSafety #MicrosoftAzure #ConnectionString
📌الگوی Saga چی هست و چرا بهش نیاز داریم؟
[Saga distributed transactions pattern]


🔹الگوی طراحی Saga به حفظ سازگاری داده‌ها در سیستم‌های توزیع‌شده با هماهنگ‌سازی تراکنش‌ها بین چندین سرویس کمک می‌کند.

🔹یک Saga دنباله‌ای از تراکنش‌های محلی (local transactions) است که در آن هر سرویس عملیات خود را انجام داده و مرحله بعد را از طریق رویدادها یا پیام‌ها آغاز می‌کند

🔹اگر مرحله‌ای از این دنباله با شکست مواجه شود، Saga مجموعه‌ای از تراکنش‌های جبرانی (compensating transactions) را برای برگرداندن مراحل انجام‌شده اجرا می‌کند. این روش به حفظ سازگاری داده‌ها کمک می‌کند.

📍زمینه و مشکل (Context and problem)


🔹یک تراکنش، واحدی از کار است که می‌تواند شامل چندین عملیات باشد.

🔹در یک تراکنش، رویداد (event) به تغییر وضعیت اشاره دارد که بر یک موجودیت تأثیر می‌گذارد.

🔹یک فرمان (command) تمام اطلاعات لازم برای انجام یک عمل یا ایجاد رویداد بعدی را در خود دارد.

⚖️تراکنش‌ها باید به اصول ACID پایبند باشند:


1️⃣ اتمی بودن (Atomicity): همه عملیات‌ها موفق می‌شوند یا هیچ عملیاتی موفق نمی‌شود.

2️⃣ سازگاری (Consistency): داده‌ها از یک حالت معتبر به حالت معتبر دیگری منتقل می‌شوند.

3️⃣ انزوا (Isolation): تراکنش‌های همزمان نتایجی مشابه اجرای ترتیبی ایجاد می‌کنند.

4️⃣ ماندگاری (Durability): تغییرات پس از ثبت، حتی در صورت بروز خطا، باقی می‌مانند.
در یک سرویس واحد، تراکنش‌ها اصول ACID را رعایت می‌کنند زیرا در یک پایگاه داده واحد عمل می‌کنند. با این حال، دستیابی به انطباق ACID در چندین سرویس دشوارتر است.

🏗چالش‌ها در معماری‌های مایکروسرویس
(Challenges in microservices architectures)


معماری‌های مایکروسرویس معمولاً یک پایگاه داده اختصاصی به هر سرویس اختصاص می‌دهند. این روش مزایای زیر را دارد:

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

⚠️با وجود این مزایا، این معماری سازگاری داده بین سرویس‌ها را پیچیده می‌کند. تضمین‌های سنتی پایگاه داده مانند ACID به طور مستقیم در چندین پایگاه داده مستقل قابل اجرا نیستند. به همین دلیل، معماری‌هایی که به ارتباط بین پردازشی (IPC) یا مدل‌های تراکنش سنتی مانند two-phase commit protocol وابسته‌اند، اغلب برای الگوی Saga مناسب‌تر هستند.
💡راهکار (Solution)


الگوی Saga تراکنش‌ها را با تقسیم آن‌ها به مجموعه‌ای از تراکنش‌های محلی مدیریت می‌کند.
هر تراکنش محلی:

• کار خود را به‌صورت اتمی در یک سرویس انجام می‌دهد.
• پایگاه داده سرویس را به‌روزرسانی می‌کند.
• تراکنش بعدی را از طریق رویداد یا پیام آغاز می‌کند.
• اگر یک تراکنش محلی شکست بخورد، Saga مجموعه‌ای از تراکنش‌های جبرانی را اجرا می‌کند تا تغییرات تراکنش‌های قبلی را برگرداند.


🔑 مفاهیم کلیدی در الگوی Saga
(Key concepts in the Saga pattern)


تراکنش‌های قابل جبران (Compensable Transactions):

تراکنش‌هایی که می‌توان آن‌ها را توسط تراکنش‌های دیگر با اثر معکوس، جبران یا لغو کرد. اگر یک مرحله شکست بخورد، تراکنش‌های جبرانی تغییرات اعمال‌شده را برمی‌گردانند.

تراکنش محوری (Pivot Transaction):

نقطه بدون بازگشت در Saga است. بعد از موفقیت تراکنش محوری، تراکنش‌های جبرانی دیگر کاربردی ندارند و همه اقدامات بعدی باید برای رسیدن سیستم به حالت سازگار نهایی تکمیل شوند. تراکنش محوری می‌تواند:

• آخرین تراکنش قابل جبران باشد.
•اولین عملیات قابل تکرار (Retryable) در Saga باشد.

تراکنش‌های قابل تکرار (Retryable Transactions):

پس از تراکنش محوری اجرا می‌شوند، idempotent هستند و تضمین می‌کنند که حتی در صورت بروز خطاهای موقت، Saga به حالت سازگار برسد.
رویکردهای پیاده‌سازی Saga
(Saga implementation approaches)


دو رویکرد رایج برای پیاده‌سازی Saga وجود دارد: Choreography و Orchestration.

1️⃣ Choreography

در این رویکرد، سرویس‌ها بدون کنترل‌کننده مرکزی، از طریق رویدادها با هم تعامل دارند. هر تراکنش محلی یک رویداد دامنه منتشر می‌کند که تراکنش محلی دیگری در سرویس دیگر را فعال می‌کند.

مزایا:
مناسب برای جریان‌های کاری ساده با سرویس‌های کم و بدون نیاز به منطق هماهنگی.

عدم نیاز به سرویس اضافی برای هماهنگی.

بدون نقطه شکست مرکزی (distributed responsibility).

معایب:
⚠️با اضافه شدن مراحل جدید، جریان کاری پیچیده می‌شود.

⚠️ریسک ایجاد وابستگی حلقه‌ای بین سرویس‌ها.

⚠️تست یکپارچگی دشوار به دلیل نیاز به اجرای همه سرویس‌ها برای شبیه‌سازی تراکنش.
2️⃣ Orchestration

در این رویکرد، یک کنترل‌کننده مرکزی (Orchestrator) مسئول مدیریت همه تراکنش‌ها است و بر اساس رویدادها به سرویس‌ها می‌گوید چه عملیاتی انجام دهند.

مزایا:
مناسب برای جریان‌های کاری پیچیده یا زمانی که سرویس‌های جدید اضافه می‌شوند.

جلوگیری از وابستگی حلقه‌ای.

جداسازی واضح وظایف.

معایب:
⚠️ نیاز به پیاده‌سازی منطق هماهنگی (design complexity).

⚠️وجود نقطه شکست مرکزی.
🛠مشکلات و ملاحظات
(Problems and considerations)


● تغییر در تفکر طراحی (Design Thinking Shift)

● دشواری در دیباگ به‌خصوص با افزایش سرویس‌ها

● غیرقابل بازگشت بودن تغییرات محلی دیتابیس

● نیاز به مدیریت خطاهای موقت و تضمین idempotency

● نیاز به مانیتورینگ و ردیابی جریان Saga

● محدودیت در تراکنش‌های جبرانی (همیشه موفق نمی‌شوند)


⚠️ناهنجاری‌های داده‌ای در Saga
(Potential data anomalies in sagas)


به دلیل مدیریت مستقل داده‌ها توسط سرویس‌ها، نبود ایزوله‌سازی بین سرویس‌ها می‌تواند منجر به ناسازگاری داده شود:

📍به‌روزرسانی‌های از دست رفته (Lost Updates)

📍خواندن کثیف (Dirty Reads)

📍خواندن غیرتکرارپذیر (Nonrepeatable Reads)

🛡راهکارهای جلوگیری از ناهنجاری‌ها
(Strategies to address data anomalies)


🔐 قفل معنایی (Semantic Lock)

🔄 به‌روزرسانی‌های جابجاپذیر (Commutative Updates)

🧑‍💻 دید بدبینانه (Pessimistic View)

📖 خواندن مجدد مقادیر (Reread Values)

🗃 نسخه‌بندی رکوردها (Version Files)


📌زمان استفاده از این الگو
(When to use this pattern)


مناسب:

✔️نیاز به سازگاری داده در سیستم توزیع‌شده بدون کوپلینگ شدید

✔️نیاز به جبران تغییرات در صورت شکست یک عملیات

نامناسب:

تراکنش‌های به شدت وابسته

نیاز به جبران تراکنش‌ها در مراحل اولیه

وجود وابستگی حلقه‌ای


🔖 هشتگ‌ها:
#Microservices #SagaPattern #SoftwareArchitecture
10 نکات قبل از برنامه نویس شدن🧑‍💻
استفاده از چند EF Core DbContext در یک برنامه

Entity Framework Core (EF Core)
یک ORM محبوب در NET. است که به شما امکان می‌دهد با پایگاه‌ داده‌های SQL کار کنید.

EF Core
از DbContext استفاده می‌کند که نمایانگر یک نشست (Session) با پایگاه داده است و مسئول ردیابی تغییرات، انجام عملیات دیتابیس و مدیریت اتصال‌های پایگاه داده می‌باشد.

به‌طور معمول، برای کل برنامه فقط یک DbContext استفاده می‌شود.
اما اگر نیاز داشته باشید چندین DbContext داشته باشید چه می‌کنید؟ 🤔

📍در این مطلب بررسی می‌کنیم:

• چه زمانی ممکن است بخواهید از چندین DbContext استفاده کنید؟

• چطور چندین DbContext ایجاد کنیم؟

• مزایای استفاده از چندین DbContext؟

📌 چرا از چندین DbContext استفاده کنیم؟


1️⃣ چندین پایگاه داده
اگر برنامه شما باید با چندین دیتابیس SQL کار کند، مجبورید از چندین DbContext استفاده کنید که هر کدام مختص یک دیتابیس مشخص هستند.

2️⃣ تفکیک مسئولیت‌ها
اگر مدل دامنه (Domain Model) برنامه شما پیچیده است، ممکن است با تفکیک وظایف بین چند DbContext عملکرد بهتری بگیرید؛ به طوری که هر DbContext مسئول یک بخش خاص از دامنه باشد.

3️⃣ معماری Modular Monolith
در این نوع معماری می‌توانید برای هر DbContext یک Schema متفاوت در پایگاه داده پیکربندی کنید تا جداسازی منطقی در سطح دیتابیس داشته باشید.

4️⃣ استفاده از Read Replicas
می‌توانید یک DbContext جداگانه برای دسترسی به Read Replica پایگاه داده تنظیم کنید و از آن فقط برای کوئری‌های خواندنی استفاده کنید. همچنین می‌توانید ویژگی QueryTrackingBehavior.NoTracking را برای آن فعال کنید تا عملکرد بهتری داشته باشید.

⚙️ ایجاد چندین DbContext در یک برنامه


فرض کنید در برنامه خود دو DbContext داریم:
• CatalogDbContext
• OrderDbContext

و می‌خواهیم شرایط زیر را رعایت کنیم:

• هر دو از یک دیتابیس استفاده کنند
• هر DbContext Schema متفاوتی داشته باشد
public class CatalogDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
}

public class OrderDbContext : DbContext
{
public DbSet<Order> Orders { get; set; }
public DbSet<LineItem> LineItems { get; set; }
}


ابتدا باید DbContext‌ها را در DI Container پیکربندی کنیم:
services.AddDbContext<CatalogDbContext>(options =>
options.UseSqlServer("CONNECTION_STRING"));

services.AddDbContext<OrderDbContext>(options =>
options.UseSqlServer("CONNECTION_STRING"));

اگر هر دو در یک Schema باشند، همین کافی است.
اما اگر بخواهید Schema جداگانه داشته باشید، باید در متد OnModelCreating این مورد را مشخص کنید:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("catalog");
}


و برای DbContext دوم:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("order");
}


⚠️ محدودیت‌ها در استفاده از چندین DbContext


• جوین (Join) بین DbContext‌های مختلف ممکن نیست.

• تراکنش‌ها فقط زمانی کار می‌کنند که هر دو DbContext از یک دیتابیس استفاده کنند.

• اگر Schema‌های جدا دارید، باید جدول تاریخچه مایگریشن‌ها را نیز جداگانه تعریف کنید:
services.AddDbContext<CatalogDbContext>(options =>
options.UseSqlServer(
"CONNECTION_STRING",
o => o.MigrationsHistoryTable(
tableName: HistoryRepository.DefaultTableName,
schema: "catalog")));


مزایای استفاده از چندین DbContext


تفکیک مسئولیت‌ها → کد سازمان‌یافته‌تر و ماژولارتر

عملکرد بهتر → کاهش تداخل و بهبود همزمانی

کنترل و امنیت بیشتر → امکان تنظیم دسترسی دقیق‌تر

📝 جمع‌بندی
استفاده از چند DbContext در EF Core ساده و کاربردی است.

برای برنامه‌های Read-Heavy می‌توانید DbContext جداگانه‌ای بدون Query Tracking داشته باشید.

در معماری Modular Monolith می‌توانید DbContext‌ها را با Schema جداگانه پیکربندی کنید تا جداسازی منطقی در دیتابیس داشته باشید.

🔖هشتگ‌ها:
#DotNet #EFCore #CSharp #DbContext #ModularMonolith #EntityFramework #DatabaseDesign
💡 نکته عملکردی C#/.NET – آرایه‌های Inline 🔥


💎 آرایه‌های Inline چیستند؟

⚡️ در C# 12 و NET 8.0. معرفی شده‌اند. آرایه‌های inline به ما اجازه می‌دهند که یک آرایه با اندازه ثابت در یک نوع ساختاری (struct) ایجاد کنیم. این آرایه‌ها توسط تیم Runtime و نویسندگان دیگر کتابخانه‌ها برای بهبود عملکرد در برنامه‌های شما استفاده می‌شوند.

⚡️ از نظر عملکرد در کنسول، تغییر خاصی در کارکرد ایجاد نشده است. یک struct با یک آرایه inline باید ویژگی‌های عملکردی مشابه یک بافر ثابت (fixed size buffer) ناایمن (unsafe) داشته باشد.

💡 برخلاف آرایه‌های پویا (dynamic) سنتی، آرایه‌های inline در فضای حافظه همان struct قرار می‌گیرند. این جای‌گیری منحصربه‌فرد چند مزیت کلیدی را فراهم می‌کند.

چند مزیت آرایه‌های Inline:

🔸 بهبود عملکرد: با حذف تخصیص حافظه روی heap و استفاده از حافظه stack، آرایه‌های inline سرعت اجرای توابع را به شکل قابل‌توجهی افزایش می‌دهند و فشار کلی روی حافظه را کاهش می‌دهند.
🔸 مدیریت حافظه ساده‌تر: دیگر نیازی به تخصیص صریح یا نگرانی درباره جمع‌آوری زباله (Garbage Collection) نیست. آرایه‌های inline به‌طور یکپارچه در structها ادغام می‌شوند و شما را از دردسر مدیریت حافظه رها می‌کنند.
🔸 ایمنی نوع قوی‌تر: بررسی‌های زمان کامپایل برای اندازه آرایه و نوع عناصر، یک لایه حفاظتی اضافی در برابر خطاهای زمان اجرا ایجاد می‌کنند.

🔖هشتگ‌ها:
#csharp #dotnet #programming #softwareengineering #softwaredevelopment
📨 معماری رویدادمحور (Event-Driven Architecture) در NET. با RabbitMQ


⚡️ معماری رویدادمحور (EDA) می‌تواند برنامه‌ها را منعطف‌تر و قابل‌اعتمادتر کند.
به جای اینکه یک بخش سیستم مستقیماً بخش دیگر را فراخوانی کند، اجازه می‌دهیم رویدادها از طریق پیام‌رسان (Message Broker) جریان پیدا کنند.

📚 در این راهنمای سریع، یک سیستم ساده رویدادمحور در NET. با استفاده از RabbitMQ پیاده‌سازی می‌کنیم.

📌 سناریوی ما

یک تولیدکننده (Producer) رویدادها را ارسال می‌کند و یک مصرف‌کننده (Consumer) آن‌ها را دریافت می‌کند.
برای تست، RabbitMQ را در یک کانتینر Docker اجرا می‌کنیم (با فعال بودن رابط کاربری مدیریت (Management UI) تا بتوانیم فعالیت‌ها را مشاهده کنیم).

از پکیج رسمی RabbitMQ.Client در یک اپلیکیشن کنسول NET. استفاده خواهیم کرد.

🐳 اجرای RabbitMQ با Docker

اگر RabbitMQ را نصب نکرده‌اید، می‌توانید آن را به سرعت با Docker اجرا کنید:
docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4-management

📍 این دستور یک RabbitMQ Broker روی localhost راه‌اندازی می‌کند:

🔌 پورت 5672 → پروتکل AMQP

🌐 پورت 15672 → رابط مدیریت در آدرس:
http://localhost:15672

🛠 مفاهیم پایه RabbitMQ

🏭 Producer → برنامه‌ای که پیام‌ها (رویدادها) را به RabbitMQ ارسال می‌کند.

📥 Consumer → برنامه‌ای که پیام‌ها را از یک صف دریافت می‌کند.

📦 Queue → مانند یک صندوق پستی که پیام‌ها را ذخیره می‌کند. مصرف‌کنندگان از صف‌ها می‌خوانند.

🔀 Exchange → مکانیسم مسیردهی که پیام‌های دریافتی از تولیدکنندگان را به صف‌ها هدایت می‌کند.

💡 نکته: در RabbitMQ، تولیدکنندگان هرگز مستقیماً به یک صف ارسال نمی‌کنند، بلکه به یک Exchange ارسال می‌کنند. Exchange تعیین می‌کند که پیام به کدام صف یا صف‌ها برود.
🚀 تولیدکننده (Producer) – ارسال رویداد
فرض کنید رویداد OrderPlaced داریم که می‌تواند سرویس‌های پایین‌دستی مثل انبار، ایمیل اطلاع‌رسانی و غیره را فعال کند.
var factory = new ConnectionFactory() { HostName = "localhost" };
using var connection = await factory.CreateConnectionAsync();
using var channel = await connection.CreateChannelAsync();

await channel.QueueDeclareAsync(
queue: "orders",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);

var orderPlaced = new OrderPlaced
{
OrderId = Guid.NewGuid(),
Total = 99.99,
CreatedAt = DateTime.UtcNow
};
var message = JsonSerializer.Serialize(orderPlaced);
var body = Encoding.UTF8.GetBytes(message);

await channel.BasicPublishAsync(
exchange: string.Empty,
routingKey: "orders",
mandatory: true,
basicProperties: new BasicProperties { Persistent = true },
body: body);

Console.WriteLine($"Sent: {message}");


📌 نکات مهم:


📂 صف durable است → بعد از ریست RabbitMQ باقی می‌ماند.

💾 پیام Persistent است → روی دیسک ذخیره می‌شود.

🔤 داده‌ها به JSON سریالایز شده و به بایت UTF-8 ارسال می‌شوند.

🎯 مصرف‌کننده (Consumer) – دریافت رویداد
مصرف‌کننده به همان صف متصل می‌شود و پیام‌ها را دریافت می‌کند:
var factory = new ConnectionFactory() { HostName = "localhost" };
using var connection = await factory.CreateConnectionAsync();
using var channel = await connection.CreateChannelAsync();

await channel.QueueDeclareAsync(
queue: "orders",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);

var consumer = new AsyncEventingBasicConsumer(channel);
consumer.ReceivedAsync += async (sender, eventArgs) =>
{
byte[] body = eventArgs.Body.ToArray();
string message = Encoding.UTF8.GetString(body);
var orderPlaced = JsonSerializer.Deserialize<OrderPlaced>(message);

Console.WriteLine($"Received: OrderPlaced - {orderPlaced.OrderId}");

await ((AsyncEventingBasicConsumer)sender)
.Channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false);
};
await channel.BasicConsumeAsync("orders", autoAck: false, consumer);

Console.WriteLine("Waiting for messages...");


📌 نکات مهم:


autoAck: false → پیام فقط پس از پردازش موفق تأیید می‌شود.

♻️ اگر پردازش شکست بخورد، می‌توان با BasicNack آن را مجدداً در صف قرار داد یا به Dead-Letter Queue فرستاد.
⚖️ الگوی Competing Consumers – مقیاس‌پذیری
وقتی چند مصرف‌کننده روی یک صف باشند:

هر پیام فقط به یک مصرف‌کننده تحویل داده می‌شود.

📊و RabbitMQ پیام‌ها را به صورت Round-Robin بین مصرف‌کنندگان تقسیم می‌کند.

💪 مناسب برای توزیع بار پردازش بین چند Worker.

📡 Fanout Exchange – پخش پیام به همه مصرف‌کنندگان

وقتی می‌خواهید همه سرویس‌ها پیام را دریافت کنند:

هر مصرف‌کننده صف اختصاصی خود را دارد.

و Exchange از نوع Fanout پیام را کپی کرده و به همه صف‌های متصل ارسال می‌کند.

📝 کد Producer – Fanout
await channel.ExchangeDeclareAsync(
exchange: "orders",
durable: true,
autoDelete: false,
type: ExchangeType.Fanout);

await channel.BasicPublishAsync(
exchange: "orders",
routingKey: string.Empty,
mandatory: true,
basicProperties: new BasicProperties { Persistent = true },
body: body);

📝 کد Consumer – Fanout
await channel.ExchangeDeclareAsync(
exchange: "orders",
durable: true,
autoDelete: false,
type: ExchangeType.Fanout);

await channel.QueueDeclareAsync(
queue: "orders-consumer-1",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);

await channel.QueueBindAsync("orders-consumer-1", "orders", string.Empty);

📈 گام‌های بعدی
🎯 استفاده از Direct Exchange یا Topic Exchange برای مسیردهی دقیق‌تر.

🔄 پیاده‌سازی Retry Policy و Dead-Letter Queue برای مدیریت خطاها.

📊 پایش و مانیتورینگ پیام‌ها با Management UI.

🏷 هشتگ‌ها:
#RabbitMQ #DotNet #EventDrivenArchitecture #MessageBroker