مپ کردن اسرار به یک 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
استفاده از چند 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
📝 کد Consumer – Fanout
📈 گامهای بعدی
🎯 استفاده از Direct Exchange یا Topic Exchange برای مسیردهی دقیقتر.
🔄 پیادهسازی Retry Policy و Dead-Letter Queue برای مدیریت خطاها.
📊 پایش و مانیتورینگ پیامها با Management UI.
وقتی چند مصرفکننده روی یک صف باشند:
هر پیام فقط به یک مصرفکننده تحویل داده میشود.
📊و 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
🚀 جادوی async/await در #C:
برنامهنویسی غیرهمزمان به زبان ساده
تا حالا شده یه دکمه تو اپلیکیشنتون بزنید و کل برنامه برای چند ثانیه هنگ کنه و سفید بشه؟ 🥶 این اتفاق وقتی میفته که یه کار زمانبر (مثل دانلود فایل یا کوئری دیتابیس) رو به صورت همزمان (Synchronous) انجام میدید و "نخ" اصلی برنامه رو قفل میکنید.
راه حل این کابوس، برنامهنویسی غیرهمزمان (Asynchronous) با دو کلمه کلیدی جادویی async و await هست.
1️⃣ داستان سرآشپز صبور (آنالوژی) 👨🍳
برای درک این مفهوم، یه رستوران رو تصور کنید:
سرآشپز همزمان (Synchronous): 👎
شما سفارش استیک میدید. سرآشپز استیک رو روی گریل میذاره و همونجا وایمیسته و ۱۰ دقیقه بهش زل میزنه تا بپزه. تو این ۱۰ دقیقه، هیچ کار دیگهای نمیتونه بکنه و بقیه مشتریها گشنه میمونن. این یعنی قفل شدن برنامه!
سرآشپز غیرهمزمان (Asynchronous): 👍
شما سفارش استیک میدید. سرآشپز استیک رو روی گریل میذاره (await) و بلافاصله از آشپزخونه میاد بیرون و سفارش بقیه رو میگیره. اون یه "قول" (Task) داره که استیک در حال آماده شدنه. ۱۰ دقیقه بعد، وقتی استیک آماده شد، برمیگرده سراغش، کار رو تموم میکنه و به شما تحویل میده. تو این مدت، رستوران (برنامه) کاملاً فعال و پاسخگو بوده!
2️⃣ کلمات کلیدی جادویی: async, await, Task ✨
این سه تا با هم کار میکنن:
• async:
یه برچسبه که به متد میزنیم و به کامپایلر میگیم: "هی، این متد ممکنه وسط کارش منتظر یه چیزی بمونه و غیرهمزمانه". این کلمه، اجازه استفاده از await رو داخل متد میده.
• Task / Task<T>:
"قول" یا "رسیدی" هست که متد async فوراً برمیگردونه.
• Task:
یعنی "قول میدم این کار رو تموم کنم." (برای متدهایی که خروجی ندارن).
• Task<T>:
یعنی "قول میدم این کار رو تموم کنم و یه نتیجه از نوع T بهت تحویل بدم."
• await:
قلب ماجراست! این کلمه رو قبل از صدا زدن یه متد async دیگه میذاریم و به برنامه میگه:
"اجرای این کار زمانبر رو شروع کن، و تا وقتی تموم میشه، کنترل رو به کسی که منو صدا زده برگردون تا اون بتونه به کاراش برسه! وقتی کارم تموم شد، از همین خط به بعد ادامه میدم."
3️⃣ مثال عملی: دانلود کردن یک فایل
روش بد (Synchronous) که باعث هنگ کردن برنامه میشه:
// ❌ این متد نخ اصلی رو برای ۵ ثانیه قفل میکنه!
public void DownloadFile()
{
Thread.Sleep(5000); // شبیهسازی یک عملیات زمانبر و مسدودکننده
Console.WriteLine("Download complete.");
}
روش خوب (Asynchronous) با async/await:
// ✅ این متد نخ اصلی رو آزاد میکنه و برنامه پاسخگو باقی میمونه
public async Task DownloadFileAsync()
{
// await کنترل رو به بیرون برمیگردونه و منتظر میمونه
await Task.Delay(5000); // شبیهسازی یک عملیات غیرهمزمان
Console.WriteLine("Download complete.");
}
🔖 هشتگها:
#CSharp #Programming #Developer #DotNet #Async #Await #Concurrency
کتابخانه جدید کشینگ 🆕
HybridCache در ASP.NET Core
کشینگ ⚡️ برای ساخت اپلیکیشنهای سریع و مقیاسپذیر ضروری است. ASP.NET Core به طور سنتی دو گزینه کشینگ ارائه میداد: کشینگ درون-حافظهای و کشینگ توزیعشده. هر کدام مزایا و معایب خود را داشتند. کشینگ درون-حافظهای با استفاده از IMemoryCache سریع است اما به یک سرور واحد محدود میشود. کشینگ توزیعشده با IDistributedCache با استفاده از یک backplane در چندین سرور کار میکند.
حالا NET 9. قابلیت HybridCache را معرفی میکند، یک کتابخانه جدید که بهترینهای هر دو رویکرد را ترکیب میکند. این قابلیت از مشکلات رایج کشینگ مانند cache stampede جلوگیری میکند. همچنین ویژگیهای مفیدی مانند نامعتبرسازی بر اساس تگ و نظارت بهتر بر عملکرد را اضافه میکند.
به شما نشان خواهم داد که چگونه از HybridCache در اپلیکیشنهای خود استفاده کنید.
HybridCache چیست؟ 🤔
گزینههای کشینگ سنتی در ASP.NET Core محدودیتهایی دارند. کشینگ درون-حافظهای سریع است اما به یک سرور محدود میشود. کشینگ توزیعشده در سرورهای مختلف کار میکند اما کندتر است.
HybridCache
هر دو رویکرد را ترکیب کرده و ویژگیهای مهمی را اضافه میکند:
1️⃣ کشینگ دو سطحی (L1/L2)
• L1: کش درون-حافظهای سریع
• L2: کش توزیعشده (Redis، SQL Server و غیره)
2️⃣ محافظت در برابر cache stampede
(زمانی که درخواستهای زیادی به یکباره به کش خالی برخورد میکنند)
3️⃣ نامعتبرسازی کش بر اساس تگ
4️⃣ سریالسازی قابل پیکربندی
5️⃣ معیارها و مانیتورینگ
کش L1 در حافظه اپلیکیشن شما اجرا میشود. کش L2 میتواند Redis، SQL Server یا هر کش توزیعشده دیگری باشد. اگر به کشینگ توزیعشده نیاز ندارید، میتوانید از HybridCache فقط با کش L1 استفاده کنید.
نصب HybridCache 📦
پکیج NuGet Microsoft.Extensions.Caching.Hybrid را نصب کنید:
Install-Package
Microsoft.Extensions.Caching.Hybrid
HybridCache را به سرویسهای خود اضافه کنید:
builder.Services.AddHybridCache(options =>
{
// حداکثر اندازه آیتمهای کش شده
options.MaximumPayloadBytes = 1024 * 1024 * 10; // 10MB
options.MaximumKeyLength = 512;
// تایماوتهای پیشفرض
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(30),
LocalCacheExpiration = TimeSpan.FromMinutes(30)
};
});
برای انواع داده سفارشی، میتوانید سریالایزر خود را اضافه کنید:
builder.Services.AddHybridCache()
.AddSerializer<CustomType, CustomSerializer>();
استفاده از HybridCache 👨💻
HybridCache
چندین متد برای کار با دادههای کش شده فراهم میکند. مهمترین آنها GetOrCreateAsync، SetAsync و متدهای مختلف remove هستند. بیایید ببینیم چگونه از هر کدام در سناریوهای دنیای واقعی استفاده کنیم.
گرفتن یا ایجاد ورودیهای کش 🔎
متد GetOrCreateAsync ابزار اصلی شما برای کار با دادههای کش شده است. این متد به طور خودکار هم cache hit و هم cache miss را مدیریت میکند. اگر داده در کش نباشد، متد factory شما را برای گرفتن داده فراخوانی کرده، آن را کش میکند و برمیگرداند.
در اینجا یک endpoint برای دریافت جزئیات محصول آمده است:
app.MapGet("/products/{id}", async (
int id,
HybridCache cache,
ProductDbContext db,
CancellationToken ct) =>
{
var product = await cache.GetOrCreateAsync(
$"product-{id}",
async token =>
{
return await db.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id, token);
},
cancellationToken: ct
);
return product is null ? Results.NotFound() : Results.Ok(product);
});📌در این مثال:
• کلید کش برای هر محصول منحصر به فرد است.
• اگر محصول در کش باشد، بلافاصله برگردانده میشود.
• اگر نباشد، متد factory برای گرفتن داده اجرا میشود.
• درخواستهای همزمان دیگر برای همان محصول، منتظر پایان یافتن اولین درخواست میمانند.
گاهی اوقات نیاز دارید کش را مستقیماً بهروزرسانی کنید، مانند پس از تغییر دادهها. متد SetAsync این کار را انجام میدهد:
تگها برای مدیریت گروههایی از ورودیهای کش مرتبط، قدرتمند هستند. شما میتوانید چندین ورودی را به یکباره با استفاده از تگها نامعتبر کنید:
• نامعتبر کردن تمام محصولات در یک دستهبندی.
• پاک کردن تمام دادههای کش شده برای یک کاربر خاص.
• رفرش کردن تمام دادههای مرتبط هنگامی که چیزی تغییر میکند.
برای نامعتبرسازی مستقیم آیتمهای خاص، از RemoveAsync استفاده کنید:
برای استفاده از Redis به عنوان کش توزیعشده خود:
پکیج NuGet را نصب کنید:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
Redis و HybridCache را پیکربندی کنید:
HybridCache
کشینگ را در اپلیکیشنهای NET. ساده میکند. این قابلیت، کشینگ سریع درون-حافظهای را با کشینگ توزیعشده ترکیب میکند، از مشکلات رایج مانند cache stampede جلوگیری میکند، و هم در سیستمهای تک-سروری و هم توزیعشده به خوبی کار میکند.
با تنظیمات پیشفرض و الگوهای استفاده اولیه شروع کنید - این کتابخانه طوری طراحی شده که استفاده از آن ساده باشد در حالی که مشکلات پیچیده کشینگ را حل میکند.
• کلید کش برای هر محصول منحصر به فرد است.
• اگر محصول در کش باشد، بلافاصله برگردانده میشود.
• اگر نباشد، متد factory برای گرفتن داده اجرا میشود.
• درخواستهای همزمان دیگر برای همان محصول، منتظر پایان یافتن اولین درخواست میمانند.
تنظیم مستقیم ورودیهای کش ✍️
گاهی اوقات نیاز دارید کش را مستقیماً بهروزرسانی کنید، مانند پس از تغییر دادهها. متد SetAsync این کار را انجام میدهد:
app.MapPut("/products/{id}", async (int id, Product product, HybridCache cache) =>
{
// ابتدا دیتابیس را بهروزرسانی کنید
await UpdateProductInDatabase(product);
// سپس کش را با انقضای سفارشی بهروزرسانی کنید
var options = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromHours(1),
LocalCacheExpiration = TimeSpan.FromMinutes(30)
};
await cache.SetAsync(
$"product-{id}",
product,
options
);
return Results.NoContent();
});استفاده از تگهای کش 🏷
تگها برای مدیریت گروههایی از ورودیهای کش مرتبط، قدرتمند هستند. شما میتوانید چندین ورودی را به یکباره با استفاده از تگها نامعتبر کنید:
app.MapGet("/categories/{id}/products", async (...) =>
{
var tags = [$"category-{id}", "products"];
var products = await cache.GetOrCreateAsync(
$"products-by-category-{id}",
async token => { /* ... fetch products ... */ },
tags: tags,
cancellationToken: ct
);
return Results.Ok(products);
});
// Endpoint برای نامعتبر کردن تمام محصولات در یک دستهبندی
app.MapPost("/categories/{id}/invalidate", async (id, cache, ct) =>
{
await cache.RemoveByTagAsync($"category-{id}", ct);
return Results.NoContent();
});📍تگها برای موارد زیر مفید هستند:
• نامعتبر کردن تمام محصولات در یک دستهبندی.
• پاک کردن تمام دادههای کش شده برای یک کاربر خاص.
• رفرش کردن تمام دادههای مرتبط هنگامی که چیزی تغییر میکند.
حذف ورودیهای تکی 🗑
برای نامعتبرسازی مستقیم آیتمهای خاص، از RemoveAsync استفاده کنید:
app.MapDelete("/products/{id}", async (int id, HybridCache cache) =>
{
// ابتدا از دیتابیس حذف کنید
await DeleteProductFromDatabase(id);
// سپس از کش حذف کنید
await cache.RemoveAsync($"product-{id}");
return Results.NoContent();
});افزودن Redis به عنوان کش L2 🔥
برای استفاده از Redis به عنوان کش توزیعشده خود:
پکیج NuGet را نصب کنید:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
Redis و HybridCache را پیکربندی کنید:
// افزودن Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "your-redis-connection-string";
});
// افزودن HybridCache - به طور خودکار از Redis به عنوان L2 استفاده خواهد کرد
builder.Services.AddHybridCache();
خلاصه 📝
HybridCache
کشینگ را در اپلیکیشنهای NET. ساده میکند. این قابلیت، کشینگ سریع درون-حافظهای را با کشینگ توزیعشده ترکیب میکند، از مشکلات رایج مانند cache stampede جلوگیری میکند، و هم در سیستمهای تک-سروری و هم توزیعشده به خوبی کار میکند.
با تنظیمات پیشفرض و الگوهای استفاده اولیه شروع کنید - این کتابخانه طوری طراحی شده که استفاده از آن ساده باشد در حالی که مشکلات پیچیده کشینگ را حل میکند.
🔖 هشتگها:
#CSharp #Programming #Developer #DotNet #HybridCache
مدیریت خطای سراسری در ASP.NET Core: از Middleware تا Handlerهای مدرن 🚨
بیایید در مورد چیزی صحبت کنیم که همه ما با آن سر و کار داریم اما اغلب تا آخرین لحظه به تعویق میاندازیم - مدیریت خطا در اپلیکیشنهای ASP.NET Core ما.
وقتی چیزی در پروداکشن خراب میشود، آخرین چیزی که میخواهید یک خطای مبهم 500 بدون هیچ زمینهای است. مدیریت خطای مناسب فقط مربوط به لاگ کردن استثناها نیست. بلکه در مورد این است که مطمئن شوید اپلیکیشن شما به آرامی شکست میخورد و اطلاعات مفیدی به فراخواننده (و شما) میدهد.
در این مقاله، گزینههای اصلی برای مدیریت خطای سراسری در ASP.NET Core را بررسی خواهیم کرد.
مدیریت خطای مبتنی بر Middleware 📜
روش کلاسیک برای گرفتن استثناهای کنترلنشده، استفاده از Middleware سفارشی است. اینجاست که اکثر ما شروع میکنیم، و صادقانه بگویم، هنوز هم برای اکثر سناریوها عالی کار میکند.
internal sealed class GlobalExceptionHandlerMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionHandlerMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
logger.LogError(ex, "Unhandled exception occurred");
// حتماً قبل از نوشتن در بدنه پاسخ، کد وضعیت را تنظیم کنید
context.Response.StatusCode = ex switch
{
ApplicationException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};
await context.Response.WriteAsJsonAsync(
new ProblemDetails
{
Type = ex.GetType().Name,
Title = "An error occured",
Detail = ex.Message
});
}
}
}
فراموش نکنید که middleware را به پایپلاین درخواست اضافه کنید:
app.UseMiddleware<GlobalExceptionHandlerMiddleware>();
این رویکرد محکم است و در همه جای پایپلاین شما کار میکند. زیبایی آن در سادگیاش است: همه چیز را در یک try-catch بپیچید، خطا را لاگ کنید و یک پاسخ یکپارچه برگردانید.
اما وقتی شروع به افزودن قوانین خاص برای انواع مختلف استثناها میکنید (مانند ValidationException, NotFoundException)، این به یک آشفتگی تبدیل میشود.
معرفی IProblemDetailsService 📄
مایکروسافت این نقطه ضعف را تشخیص داد و IProblemDetailsService را برای استانداردسازی پاسخهای خطا به ما داد. به جای سریالسازی دستی آبجکتهای خطای خودمان، میتوانیم از فرمت داخلی Problem Details استفاده کنیم.
// ...
catch (Exception ex)
{
logger.LogError(ex, "Unhandled exception occurred");
context.Response.StatusCode = ex switch
{
ApplicationException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};
await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
{
HttpContext = context,
Exception = ex,
ProblemDetails = new ProblemDetails
{
Type = ex.GetType().Name,
Title = "An error occured",
Detail = ex.Message
}
});
}
// ...
این خیلی تمیزتر است. ما اکنون از یک فرمت استاندارد استفاده میکنیم که مصرفکنندگان API انتظار دارند. اما هنوز با مشکل آن switch statement در حال رشد گیر کردهایم.
روش مدرن: IExceptionHandler ✨
ASP.NET Core 8
اینترفیس IExceptionHandler را معرفی کرد، و این یک تغییردهنده بازی است. به جای یک middleware عظیم که همه چیز را مدیریت میکند، میتوانیم handlerهای متمرکزی برای انواع خاص استثناها ایجاد کنیم.
اینطور کار میکند:
internal sealed class GlobalExceptionHandler(
IProblemDetailsService problemDetailsService,
ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
logger.LogError(exception, "Unhandled exception occurred");
httpContext.Response.StatusCode = exception switch
{
ApplicationException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};
return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
{
// ...
});
}
}
نکته کلیدی در اینجا مقدار بازگشتی است. اگر handler شما بتواند استثنا را مدیریت کند، true برگردانید. اگر نه، false برگردانید و اجازه دهید handler بعدی تلاش کند.
فراموش نکنید آن را با DI و در پایپلاین درخواست ثبت کنید:
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
// و در پایپلاین شما
app.UseExceptionHandler();
این رویکرد بسیار تمیزتر است. هر handler یک وظیفه دارد و کد به راحتی قابل تست و نگهداری است.
زنجیرهای کردن Exception Handlerها ⛓️
شما میتوانید چندین exception handler را با هم زنجیرهای کنید و آنها به ترتیبی که ثبت کردهاید اجرا میشوند. ASP.NET Core از اولین handler که true از TryHandleAsync برگرداند، استفاده خواهد کرد.
مثال: یکی برای خطاهای اعتبارسنجی، یکی به عنوان fallback سراسری.
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
بیایید بگوییم شما از FluentValidation استفاده میکنید (و باید هم استفاده کنید). در اینجا یک راهاندازی کامل آمده است:
internal sealed class ValidationExceptionHandler(
IProblemDetailsService problemDetailsService,
ILogger<ValidationExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if (exception is not ValidationException validationException)
{
return false;
}
logger.LogError(exception, "Unhandled exception occurred");
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
var context = new ProblemDetailsContext { /* ... */ };
var errors = validationException.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(
g => g.Key.ToLowerInvariant(),
g => g.Select(e => e.ErrorMessage).ToArray()
);
context.ProblemDetails.Extensions.Add("errors", errors);
return await problemDetailsService.TryWriteAsync(context);
}
}
ترتیب اجرا مهم است. فریمورک هر handler را به ترتیبی که ثبت کردهاید امتحان میکند. پس handlerهای خاصتر خود را اول و handler catch-all خود را آخر قرار دهید.
خلاصه 📝
ما راه درازی را از روزهای ساخت دستی پاسخهای خطا در middleware پیمودهایم. تکامل به این شکل است:
1️⃣ Middleware:
ساده، همه جا کار میکند، اما سریع پیچیده میشود.
2️⃣ IProblemDetailsService:
فرمت پاسخ را استاندارد میکند، هنوز قابل مدیریت است.
3️⃣ IExceptionHandler:
مدرن، قابل تست، و به زیبایی مقیاسپذیر است.
نکته کلیدی؟ 🔑
اجازه ندهید مدیریت خطا یک فکر آخر باشد. آن را زود راهاندازی کنید، یکپارچهاش کنید، و کاربران شما (و خود آیندهتان) وقتی همه چیز به ناچار خراب شد، از شما تشکر خواهند کرد.
🔖 هشتگها:
#CSharp #DotNet #ErrorHandling #ExceptionHandling #SoftwareArchitecture