C# Geeks (.NET) – Telegram
💡 نکته عملکردی 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
🚀 جادوی 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 این کار را انجام می‌دهد:
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
مدیریت متمرکز پکیج‌ها (CPM) در NET. : یک بار برای همیشه!


روزهایی رو به یاد میارم که مدیریت پکیج‌های NuGet در چندین پروژه یک دردسر واقعی بود. میدونید چی میگم - یه سولوشن بزرگ رو باز می‌کنی و می‌بینی هر پروژه از یه نسخه متفاوت از همون پکیج استفاده می‌کنه. اصلاً جالب نیست! 😫

بذارید بهتون نشون بدم که چطور مدیریت متمرکز پکیج‌ها (CPM) در NET. می‌تونه این مشکل رو یک بار برای همیشه حل کنه.

مشکلی که باید حل کنیم 💥
من اغلب با سولوشن‌هایی کار می‌کنم که پروژه‌های زیادی دارن. سولوشن‌هایی با ۳۰ یا بیشتر پروژه غیرمعمول نیستن. هر کدوم به پکیج‌های مشابهی مثل Serilog یا Polly نیاز دارن. قبل از CPM، پیگیری نسخه‌های پکیج یه افتضاح بود:

🔹 یک پروژه از Serilog 4.1.0 استفاده می‌کرد.

🔹 دیگری از Serilog 4.0.2.

🔹 و یه جوری، سومی از Serilog 3.1.1!

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

مدیریت متمرکز پکیج‌ها چگونه کمک می‌کند؟ 🎮

CPM
رو مثل یک مرکز کنترل برای تمام نسخه‌های پکیج‌تون در نظر بگیرید. به جای تنظیم نسخه‌ها در هر پروژه، اون‌ها رو یک بار در یک جا تنظیم می‌کنید. بعد، فقط به پکیجی که می‌خواید استفاده کنید، بدون مشخص کردن نسخه، ارجاع میدید. به همین سادگی!

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


NuGet نسخه 6.2 یا جدیدتر

.NET SDK نسخه 6.0.300 یا جدیدتر

اگر از ویژوال استودیو استفاده می‌کنید، نسخه 2022 17.2 یا جدیدتر

راه‌اندازی آن 📁

اول، یک فایل به نام Directory.Packages.props در پوشه اصلی سولوشن خود ایجاد کنید:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Serilog" Version="4.1.0" />
<PackageVersion Include="Polly" Version="8.5.0" />
</ItemGroup>
</Project>

در فایل‌های پروژه‌تون، می‌تونید پکیج‌ها رو با استفاده از PackageReference بدون نسخه لیست کنید:
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="AutoMapper" />
<PackageReference Include="Polly" />
</ItemGroup>

همین! حالا تمام پروژه‌های شما از یک نسخه پکیج یکسان استفاده خواهند کرد.

✨️کارهای باحالی که می‌تونید انجام بدید
نیاز به نسخه‌ای متفاوت برای یک پروژه دارید؟ 🎯

مشکلی نیست! فقط این رو به فایل پروژه‌تون اضافه کنید:
<PackageReference Include="Serilog" VersionOverride="3.1.1" />

📍پراپرتی VersionOverride به شما اجازه میده نسخه خاصی که می‌خواید رو تعریف کنید.

یه پکیج رو تو همه پروژه‌ها می‌خواید؟ 🌍

اگه پکیج‌هایی دارید که هر پروژه‌ای بهشون نیاز داره، می‌تونید اون‌ها رو سراسری کنید. یک GlobalPackageReference در فایل props خود تعریف کنید:
<ItemGroup>
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" />
</ItemGroup>

حالا هر پروژه‌ای به طور خودکار این پکیج رو دریافت می‌کنه!

مهاجرت پروژه‌های موجود به CPM 🚚

1️⃣ فایل Directory.Packages.props رو در ریشه سولوشن ایجاد کنید.
2️⃣ تمام نسخه‌های پکیج رو از فایل‌های .csproj خود به اونجا منتقل کنید.
3️⃣ ویژگی Version رو از عناصر PackageReference حذف کنید.
4️⃣ سولوشن خود را بیلد کرده و هرگونه تداخل نسخه رو برطرف کنید.
5️⃣ قبل از کامیت کردن، به طور کامل تست کنید.

همچنین یه ابزار CLI به نام CentralisedPackageConverter وجود داره که می‌تونید برای اتوماتیک کردن مهاجرت ازش استفاده کنید.

چه زمانی باید از CPM استفاده کنید؟ 🤔
من دلیل قانع‌کننده‌ای برای استفاده نکردن پیش‌فرض از این قابلیت نمی‌بینم.
من توصیه می‌کنم از CPM استفاده کنید وقتی:

• پروژه‌های زیادی دارید که پکیج‌های مشترک دارن.

• از رفع باگ‌های مربوط به نسخه خسته شده‌اید.

• می‌خواید مطمئن بشید همه از نسخه‌های یکسان استفاده می‌کنن.

جمع‌بندی 📝
نکات من برای موفقیت با مدیریت متمرکز پکیج‌ها:


💡 وقتی CPM رو به یه سولوشن موجود اضافه می‌کنید، این کار رو در یک change/PR جداگانه انجام بدید.

💡 اگه نسخه‌ای رو override می‌کنید، یه کامنت بذارید که دلیلش رو توضیح بده.

💡 نسخه‌های پکیج خود را به طور منظم برای آپدیت‌ها چک کنید.

💡 فقط پکیج‌هایی رو سراسری کنید که واقعاً همه جا بهشون نیاز دارید.

🔖 هشتگ‌ها:
#CSharp #DotNet #NuGet #DependencyManagement #BestPractices #CleanCode #Developer #VisualStudio
الگوی CQRS به روشی که از ابتدا باید می‌بود 🚀


📢 MediatR
در حال تجاری شدن است.

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

و زمان بدی هم برای این کار نیست. MediatR تقریباً با CQRS در NET. مترادف شده است، با وجود این واقعیت که CQRS و MediatR یک چیز نیستند. اکثر پروژه‌ها از آن به عنوان یک لایه ارسال (dispatching) نازک برای کامندها و کوئری‌ها استفاده می‌کنند - یک مورد استفاده که می‌تواند با چند انتزاع (abstraction) ساده پوشش داده شود.

با حذف MediatR، شما به دست می‌آورید:


کنترل کامل بر زیرساخت CQRS خود
ارسال قابل پیش‌بینی و صریح به handlerها
دیباگ و آنبوردینگ ساده‌تر
راه‌اندازی تمیزتر DI و تست‌پذیری بهتر

📌ما پوشش خواهیم داد:

🔹 تعریف قراردادهای ICommand, IQuery و handlerها
🔹 افزودن پشتیبانی برای دکوراتورها (لاگینگ، اعتبارسنجی و غیره)
🔹 ثبت همه چیز با DI
🔹 یک مثال کامل و کاربردی در یک سناریوی دنیای واقعی

کامندها، کوئری‌ها و Handlerها 🧱

بیایید با تعریف قراردادهای پایه برای کامندها و کوئری‌ها شروع کنیم.
// ICommand.cs
public interface ICommand;
public interface ICommand<TResponse>;

// IQuery.cs
public interface IQuery<TResponse>;


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

اینترفیس‌های handler از همان مدل پیروی می‌کنند:
// ICommandHandler.cs
public interface ICommandHandler<in TCommand> where TCommand : ICommand
{
Task<Result> Handle(TCommand command, CancellationToken cancellationToken);
}
public interface ICommandHandler<in TCommand, TResponse> where TCommand : ICommand<TResponse>
{
Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken);
}

// IQueryHandler.cs
public interface IQueryHandler<in TQuery, TResponse> where TQuery : IQuery<TResponse>
{
Task<Result<TResponse>> Handle(TQuery query, CancellationToken cancellationToken);
}


این‌ها تقریباً با APIهای IRequest و IRequestHandler MediatR یکسان هستند، که مهاجرت را در صورت خروج از MediatR بسیار ساده می‌کند.

مثال عملی: Command Handler 👨‍💻

برای دیدن این انتزاع‌ها در عمل، بیایید یک کامند را پیاده‌سازی کنیم که یک آیتم todo را به عنوان تکمیل شده علامت‌گذاری می‌کند.
// CompleteTodoCommand.cs
public sealed record CompleteTodoCommand(Guid TodoItemId) : ICommand;

// CompleteTodoCommandHandler.cs
internal sealed class CompleteTodoCommandHandler(...) : ICommandHandler<CompleteTodoCommand>
{
public async Task<Result> Handle(CompleteTodoCommand command, CancellationToken cancellationToken)
{
// ... (منطق بیزینس برای تکمیل کردن todo) ...
return Result.Success();
}
}


💡چند نکته مهم:


• کامند یک آبجکت مقدار تغییرناپذیر است (فقط داده، بدون رفتار).

• هندلر تمام منطق بیزینس را کپسوله می‌کند: اعتبارسنجی، تغییر وضعیت، ایجاد domain events و پایداری.

• هیچ mediator، ISender یا ارسال پنهانی وجود ندارد. handler مستقیماً از طریق انتزاع‌های سفارشی ما فراخوانی می‌شود.

دکوراتورها 🎨

برای پشتیبانی از دغدغه‌های مشترک (cross-cutting concerns) مانند لاگینگ، اعتبارسنجی و تراکنش‌ها، ما الگوی دکوراتور را در اطراف handlerهای خود اعمال می‌کنیم.

بیایید به دو مثال نگاه کنیم: یکی برای لاگینگ، یکی برای اعتبارسنجی.

دکوراتور لاگینگ: 📝

internal sealed clasa LoggingCommandHandler<TCommand, TResponse>(...)
: ICommandHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
public async Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken)
{
// ... (منطق لاگ کردن قبل و بعد از اجرای handler اصلی) ...
}
}

این کلاس هر ICommandHandler را می‌پیچد و لاگینگ ساختاریافته را در اطراف اجرای کامند اضافه می‌کند.
دکوراتور اعتبارسنجی با FluentValidation:


internal sealed class ValidationCommandHandler<TCommand, TResponse>(...)
: ICommandHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
public async Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken)
{
// ... (منطق اعتبارسنجی قبل از اجرای handler اصلی) ...
}
}


⚠️ مهم: از آنجایی که ما با اینترفیس‌های جنریک کار می‌کنیم، هر دکوراتور باید صراحتاً همان قرارداد جنریک را هدف قرار دهد.

در بخش بعدی، این را با استفاده از Scrutor به هم متصل خواهیم کرد.

راه‌اندازی DI ⚙️

با handlerها و دکوراتورهایمان، می‌توانیم همه چیز را با استفاده از Scrutor ثبت کنیم.
services.Scan(scan => scan.FromAssembliesOf(typeof(DependencyInjection))
.AddClasses(classes => classes.AssignableTo(typeof(IQueryHandler<,>)), publicOnly: false)
.AsImplementedInterfaces()
.WithScopedLifetime()
// ... ثبت بقیه handlerها ...
);

🔹این کد اسمبلی اپلیکیشن را اسکن کرده و تمام command و query handlerها را ثبت می‌کند.

🔹سپس، دکوراتورها را برای اعتبارسنجی و لاگینگ اعمال می‌کنیم:
services.Decorate(typeof(ICommandHandler<,>), typeof(ValidationDecorator.CommandHandler<,>));
services.Decorate(typeof(ICommandHandler<,>), typeof(LoggingDecorator.CommandHandler<,>));

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

استفاده از Minimal API 🎯

هنگامی که همه چیز متصل شد، استفاده از یک command handler از یک endpoint Minimal API ساده است:
app.MapPut("todos/{id:guid}/complete", async (
Guid id,
ICommandHandler<CompleteTodoCommand> handler,
CancellationToken cancellationToken) =>
{
var command = new CompleteTodoCommand(id);
Result result = await handler.Handle(command, cancellationToken);
return result.Match(Results.NoContent, CustomResults.Problem);
})


ما ICommandHandler مناسب را مستقیماً به endpoint تزریق می‌کنیم. نیازی به ISender، لایه mediator یا جستجوی زمان اجرا نیست.

نتیجه‌گیری 👍

CQRS
به یک فریم‌ورک پیچیده نیاز ندارد.
با چند اینترفیس کوچک، چند کلاس دکوراتور و یک راه‌اندازی تمیز DI، می‌توانید یک پایپ‌لاین ساده و انعطاف‌پذیر برای مدیریت کامندها و کوئری‌ها بسازید. درک، تست و توسعه آن آسان است.

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

🔖 هشتگ‌ها:
#CSharp #DotNet #CQRS #SoftwareArchitecture #CleanArchitecture #DesignPatterns #BestPractices #MediatR
📖 سری آموزشی کتاب C# 12 in a Nutshell

👇کلیدواژه this در #C:

'خود' آبجکت کیست؟ در دنیای شیءگرایی، گاهی وقتا یه آبجکت نیاز داره به "خودش" اشاره کنه. ابزار #C برای این کار، کلمه کلیدی ساده ولی قدرتمند this هست. this در واقع ضمیر "من" برای یک آبجکته و به نمونه فعلی (current instance) خودش اشاره می‌کنه.

1️⃣ کاربرد اول: رفع ابهام (Disambiguation)
این رایج‌ترین کاربرد this هست. وقتی اسم پارامتر سازنده، هم‌اسم یکی از فیلدهای کلاس باشه، برای اینکه به کامپایلر بفهمونیم منظورمون فیلد کلاسه، از this استفاده می‌کنیم.
public class Test
{
private string name;
public Test(string name)
{
// this.name به فیلد کلاس اشاره داره
// name به پارامتر ورودی اشاره داره
this.name = name;
}
}


2️⃣ کاربرد دوم: پاس دادن خودِ آبجکت 🤝
گاهی وقتا لازمه یه آبجکت، رفرنس خودش رو به یه آبجکت یا متد دیگه بده.
public class Panda
{
public Panda Mate;
public void Marry(Panda partner)
{
Mate = partner;
// 'خودم' رو به عنوان جفتِ شریکم معرفی می‌کنم
partner.Mate = this;
}
}


قانون مهم ⚠️

this
فقط در اعضای غیر استاتیک (non-static) یک کلاس یا struct معتبره.

🔖 هشتگ‌ها:
#CSharp #Programming #Developer #DotNet #OOP #ThisKeyword
📖 سری آموزشی کتاب C# 12 in a Nutshell

🏦 پراپرتی‌ها (Properties) در #C:

دروازه‌های هوشمند کلاس شما چرا تو کدنویسی حرفه‌ای، تقریباً هیچوقت فیلدهای یه کلاس رو public نمی‌کنن؟ چون این کار یعنی از دست دادن کنترل! هر کسی از بیرون می‌تونه هر مقدار نامعتبری رو توش بریزه.

راه حل #C برای این مشکل، یه قابلیت فوق‌العاده به اسم پراپرتی (Property) هست.

1️⃣ پراپرتی چیست؟ فیلد با ماسک متد! 🎭

پراپرتی‌ها از بیرون شبیه فیلدهای معمولی به نظر میرسن و به همون سادگی استفاده میشن، ولی در داخل، در واقع متدهای خاصی هستن که بهشون اکسسور (accessor) گفته میشه. این به ما کنترل کامل روی خوندن و نوشتن مقدار رو میده.
Stock msft = new Stock();
msft.CurrentPrice = 30; // اکسسور set صدا زده میشه

msft.CurrentPrice -= 3;

Console.WriteLine(msft.CurrentPrice); // اکسسور get صدا زده میشه


2️⃣ کالبدشکافی یک پراپرتی 🔬

در رایج‌ترین حالت، یک پراپرتی از دو بخش تشکیل شده:

• فیلد پشتیبان (Backing Field): یک فیلد private که داده واقعی رو نگه میداره.

• پراپرتی public: دروازه‌ای که به دنیای بیرون اجازه دسترسی کنترل‌شده به اون فیلد رو میده.

✨️این پراپرتی، دو اکسسور داره:

🔹️get:
وقتی پراپرتی رو می‌خونیم، این بلوک اجرا میشه.

🔹️set:
وقتی مقداری رو به پراپرتی اختصاص میدیم، این بلوک اجرا میشه. کلمه کلیدی value در اینجا، به مقداری که داره ست میشه، اشاره داره.
public class Stock
{
// ۱. فیلد پشتیبان (private)
private decimal _currentPrice;
// ۲. پراپرتی عمومی (public)
public decimal CurrentPrice
{
get { return _currentPrice; }
set { _currentPrice = value; }
}
}


3️⃣ قدرت واقعی: کپسوله‌سازی (Encapsulation) 🛡

جادوی پراپرتی‌ها اینجاست که می‌تونید تو اکسسورهای get و set، منطق دلخواهتون رو پیاده کنید. مثلاً اعتبارسنجی (validation).
public class Stock
{
private decimal _currentPrice;
public decimal CurrentPrice
{
get { return _currentPrice; }
set
{
// منطق اعتبارسنجی
if (value < 0)
{
throw new ArgumentException("Price cannot be negative!");
}
_currentPrice = value;
}
}
}


🤔 حرف حساب و قانون طلایی
قانون طلایی شیءگرایی: فیلدها رو همیشه private نگه دارید و با پراپرتی‌های public اون‌ها رو در معرض دید بذارید. این کار به شما کنترل کامل روی کلاس‌هاتون میده و اساس کپسوله‌سازیه.

🔖 هشتگ‌ها:
#CSharp #Programming #DotNet #OOP #Properties #Encapsulation
⚡️ پست‌های سریالی جدید: Background Tasks در NET.

بچه‌ها سلام! یه مبحث خیلی خفن و کاربردی براتون آماده کردم 😎
توی دنیای واقعی برنامه‌نویسی، خیلی وقت‌ها لازمه یه سری کارها رو در پس‌زمینه (Background) انجام بدیم. کارهایی مثل:
* ارسال ایمیل و نوتیفیکیشن
* پردازش داده‌های حجیم
* بک‌آپ‌گیری از دیتابیس
* و خیلی چیزای دیگه...
برای همین، یه پست جامع و حسابی در مورد Background Tasks توی NET. آماده کردم.
این پست رو توی ۲ یا ۳ بخش منتشر می‌کنم که از مقدماتی‌ترین مفاهیم شروع می‌کنیم و قدم به قدم تا پیشرفته‌ترین تکنیک‌ها و بهترین روش‌ها پیش می‌ریم.
مطمئن باشید این مبحث به شدت توی پروژه‌های واقعی به دردتون می‌خوره!
اجرای تسک‌های پس‌زمینه (Background Tasks) در ASP.NET Core ⚙️


در این مقاله در مورد اجرای تسک‌های پس‌زمینه در ASP.NET Core صحبت خواهیم کرد. پس از خواندن این مقاله، شما قادر خواهید بود یک تسک پس‌زمینه را راه‌اندازی کرده و در عرض چند دقیقه آن را اجرا کنید.

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

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

تسک‌های پس‌زمینه با IHostedService 🔌

شما می‌توانید یک تسک پس‌زمینه را با پیاده‌سازی اینترفیس IHostedService تعریف کنید. این اینترفیس فقط دو متد دارد.
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}


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

برای پیکربندی تسک پس‌زمینه باید متد AddHostedService را فراخوانی کنید:
builder.Services.AddHostedService<MyBackgroundTask>();

فراخوانی AddHostedService، تسک پس‌زمینه را به عنوان یک سرویس singleton پیکربندی می‌کند.

پس آیا تزریق وابستگی در پیاده‌سازی‌های IHostedService هنوز کار می‌کند؟
بله، اما شما فقط می‌توانید سرویس‌های transient یا singleton را تزریق کنید.

با این حال، من دوست ندارم خودم اینترفیس IHostedService را پیاده‌سازی کنم. در عوض، ترجیح می‌دهم از کلاس BackgroundService استفاده کنم.

تسک‌های پس‌زمینه با BackgroundService 👍


کلاس BackgroundService از قبل اینترفیس IHostedService را پیاده‌سازی کرده است و یک متد abstract دارد که شما باید آن را override کنید - ExecuteAsync. وقتی از کلاس BackgroundService استفاده می‌کنید، فقط باید به عملیاتی که می‌خواهید پیاده‌سازی کنید فکر کنید.

در اینجا یک مثال از تسک پس‌زمینه برای اجرای مایگریشن‌های EF آمده است:
public class RunEfMigrationsBackgroundTask : BackgroundService
{
private readonly IServiceProvider _serviceProvider;

public RunEfMigrationsBackgroundTask(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using IServiceScope scope = _serviceProvider.CreateScope();

await using AppDbContext dbContext =
scope.ServiceProvider.GetRequiredService<AppDbContext>();

await dbContext.Database.MigrateAsync(stoppingToken);
}
}

DbContext
در EF یک سرویس scoped است، که ما نمی‌توانیم آن را مستقیماً داخل RunEfMigrationsBackgroundTask تزریق کنیم. ما باید یک نمونه از IServiceProvider را تزریق کنیم که می‌توانیم از آن برای ایجاد یک service scope سفارشی استفاده کنیم، تا بتوانیم AppDbContext scoped را resolve کنیم.

من توصیه نمی‌کنم RunEfMigrationsBackgroundTask را در پروداکشن اجرا کنید. مایگریشن‌های EF به راحتی می‌توانند شکست بخورند و شما با مشکل مواجه خواهید شد. با این حال، فکر می‌کنم برای توسعه محلی کاملاً مناسب است.
تسک‌های پس‌زمینه دوره‌ای (Periodic)

گاهی اوقات می‌خواهیم یک تسک پس‌زمینه را به طور مداوم اجرا کنیم و آن را برای انجام عملیاتی به صورت تکراری واداریم. برای مثال، می‌خواهیم هر ده ثانیه پیام‌ها را از یک صف مصرف کنیم. چگونه این را بسازیم؟

در اینجا یک مثال از PeriodicBackgroundTask برای شروع آمده است:
public class PeriodicBackgroundTask : BackgroundService
{
private readonly TimeSpan _period = TimeSpan.FromSeconds(5);
private readonly ILogger<PeriodicBackgroundTask> _logger;

public PeriodicBackgroundTask(ILogger<PeriodicBackgroundTask> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using PeriodicTimer timer = new PeriodicTimer(_period);
while (!stoppingToken.IsCancellationRequested &&
await timer.WaitForNextTickAsync(stoppingToken))
{
_logger.LogInformation("Executing PeriodicBackgroundTask");
}
}
}


ما از یک PeriodicTimer برای انتظار غیرهمزمان برای یک دوره زمانی معین، قبل از اجرای تسک پس‌زمینه خود استفاده می‌کنیم.

اگر به راه‌حل قوی‌تری نیاز داشتید چه؟ 🚀
تا الان باید واضح باشد که IHostedService زمانی مفید است که به تسک‌های پس‌زمینه ساده‌ای نیاز دارید که تا زمانی که اپلیکیشن شما در حال اجراست، فعال هستند.

چه می‌شود اگر بخواهید یک تسک پس‌زمینه زمان‌بندی‌شده داشته باشید که هر روز ساعت ۲ بامداد اجرا شود؟
شما احتمالاً می‌توانید چیزی شبیه این را خودتان بسازید، اما راه‌حل‌های موجودی وجود دارد که باید ابتدا آن‌ها را در نظر بگیرید.

در اینجا دو راه‌حل محبوب برای اجرای تسک‌های پس‌زمینه آمده است:

Quartz 🔥

Hangfire 🔥

📌ادامه دارد...

🔖 هشتگ‌ها:
#CSharp #DotNet #BackgroundTask #HostedService #WorkerService #SystemDesign #TaskScheduling
زمان‌بندی Background Jobها با Quartz.NET ⏱️


اگر در حال ساخت یک اپلیکیشن مقیاس‌پذیر هستید، یک نیازمندی رایج این است که برخی از کارها را در اپلیکیشن خود به یک background job منتقل کنید.

در اینجا چند مثال از آن آمده است:

📧 ارسال نوتیفیکیشن‌های ایمیل
📊 تولید گزارش‌ها
🔄 به‌روزرسانی یک کش
🖼 پردازش تصویر

🤔چگونه می‌توانید یک background job تکرارشونده در NET. ایجاد کنید؟


Quartz.NET
یک سیستم زمان‌بندی job کامل و متن‌باز است که می‌تواند از کوچکترین اپلیکیشن‌ها تا سیستم‌های بزرگ سازمانی استفاده شود.

سه مفهوم وجود دارد که باید در Quartz.NET درک کنید:

Job 📝
تسک پس‌زمینه‌ای واقعی که می‌خواهید اجرا کنید.

Trigger
تریگری که زمان اجرای یک job را کنترل می‌کند.

Scheduler 🧠
مسئول هماهنگی jobها و تریگرها.

بیایید ببینیم چگونه می‌توانیم از Quartz.NET برای ایجاد و زمان‌بندی background jobها استفاده کنیم.

افزودن سرویس میزبانی شده Quartz.NET 🔧

اولین کاری که باید انجام دهیم، نصب پکیج NuGet Quartz.NET است.

Install-Package Quartz.Extensions.Hosting

دلیل استفاده ما از این کتابخانه این است که به خوبی با NET. با استفاده از یک نمونه IHostedService یکپارچه می‌شود.
برای راه‌اندازی سرویس میزبانی شده Quartz.NET، به دو چیز نیاز داریم:

افزودن سرویس‌های مورد نیاز به کانتینر DI.
services.AddQuartz(configure =>
{
configure.UseMicrosoftDependencyInjectionJobFactory();
});

services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});

Quartz.NET
با واکشی jobها از کانتینر DI، آن‌ها را ایجاد می‌کند. این همچنین به این معنی است که شما می‌توانید از سرویس‌های scoped در jobهای خود استفاده کنید، نه فقط سرویس‌های singleton یا transient.
تنظیم گزینه WaitForJobsToComplete روی true تضمین می‌کند که Quartz.NET قبل از خروج، منتظر بماند تا jobها به آرامی تکمیل شوند.

ایجاد Background Job با IJob 👨‍💻

برای ایجاد یک background job با Quartz.NET باید اینترفیس IJob را پیاده‌سازی کنید.
این اینترفیس فقط یک متد واحد - Execute - را ارائه می‌دهد که می‌توانید کد background job خود را در آن قرار دهید.

💡چند نکته قابل توجه در اینجا:


ما از DI برای تزریق سرویس‌های ApplicationDbContext و IPublisher استفاده می‌کنیم.

Job
با DisallowConcurrentExecution دکوریت شده تا از اجرای همزمان همان job جلوگیری شود.
[DisallowConcurrentExecution]
public class ProcessOutboxMessagesJob : IJob
{
private readonly ApplicationDbContext _dbContext;
private readonly IPublisher _publisher;

public ProcessOutboxMessagesJob(
ApplicationDbContext dbContext,
IPublisher publisher)
{
_dbContext = dbContext;
_publisher = publisher;
}

public async Task Execute(IJobExecutionContext context)
{
List<OutboxMessage> messages = await _dbContext
.Set<OutboxMessage>()
.Where(m => m.ProcessedOnUtc == null)
.Take(20)
.ToListAsync(context.CancellationToken);

foreach (OutboxMessage outboxMessage in messages)
{
IDomainEvent? domainEvent = JsonConvert
.DeserializeObject<IDomainEvent>(
outboxMessage.Content,
new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
});

if (domainEvent is null)
{
continue;
}

await _publisher.Publish(domainEvent, context.CancellationToken);

outboxMessage.ProcessedOnUtc = DateTime.UtcNow;
await _dbContext.SaveChangesAsync();
}
}
}


حالا که background job آماده است، باید آن را در کانتینر DI ثبت کرده و یک تریگر اضافه کنیم که job را اجرا کند.