C# Geeks (.NET)
414 subscribers
155 photos
4 videos
159 links
Download Telegram
📌برسی تفاوت Task و ValueTask

زمانیکه تصمیم میگیریم کدهای زده شده را بهینه کنیم، اکثرا دنبال راه حل‌های جدید نمیگردیم. این مورد کاملا غریزی است؛ چرا که به‌دنبال کم‌ترین انرژی و بیشترین بازدهی هستیم؛ این طبیعت انسان است. صرفا کدهای قبلی را بازبینی میکنیم و سعی میکنیم نحوه‌ی نوشتن منطق‌های موجود را بهینه کنیم. در همین راستا درک عملکرد Task و ValueTask ‌ها شاید قدمی مهم در مورد بهینه کردن کد‌ها باشد؛ چرا استفاده درست و بجای این دو مورد می‌تواند تاثیر زیادی بر روی سرعت و استفاده از مصرف حافظه داشته باشد؟ در این مقاله سعی میکنیم تا درک درستی از این دو داشته باشیم.


✨️ء<Task<T چیست؟

ءTask یک کلاس در فضای نام System.Threading.Tasks است؛ به‌طوریکه کمک میکند تا یک قسمت از برنامه به صورت مستقل از Thread اصلی اجرا شود. به‌بیان دیگر می‌تواند یک Thread Pool را ایجاد و با توجه به روند کار، از یک مرحله‌ی اجرایی به مرحله‌ای دیگر منتقل می‌کند. همچنین هر Task می‌تواند یک مقدار برگشتی نیز داشته باشد.
این درحالی‌است که می‌تواند صرفا یک فرآیند را اجرا کند، بدون اینکه خروجی داشته باشد. به‌عبارتی دیگر اگر فرآیندی داشته باشیم که در نهایت یک شناسه را برمیگرداند، از <Task<int و اگر فرآیندی داشته باشیم که صرفا فرآیند همگام سازی داده‌های قدیمی به جدید را انجام میدهد، می‌تواند از نوع Task باشد.

همانطور که اشاره شد، Task یک کلاس است که شامل متد‌ها و فیلد‌های مختلفی می‌باشد. با استفاده از این اعضا می‌توان نحوه‌ی اجرای کدها و وضعیت‌های مختلف اجرای آن را مدیریت کرد، تا در نهایت اجرای آن کامل شود.

به دلیل اینکه Task یک class است و class ‌ها از نوع ReferenceType می‌باشند، روی حافظه‌ی Heap ذخیره می‌شوند و به‌ازای هر بار فراخوانی متدی که خروجی Task دارد، شیء Task را روی Heap ذخیره میکند. این شیء وضعیت اجرای قسمتی از کد ما را که میتواند sync یا async باشد، در خود ذخیره میکند تا در نهایت اجرای آن کامل شود.


💡نحوه استفاده از <Task<T

برای درک بهتر، یک تکه کد را با بهره بردن از Task ایجاد میکنیم :
public static class DummyWeatherProvider
{
public static async Task<Weather> Get(string city)
{
await Task.Delay(10);
var weather = new Weather
{
City = city,
Date = DateTime.Now,
AvgTempratureF = new Random().Next(5, 70)
};

return weather;
}
}

همان طور که مشخص است، کلاس موجود یک متد به نام Get دارد تا اطلاعات آب و هوای شهر مورد نظر را به صورت یک Task برگرداند. حال کد زیر را جهت بررسی تغییر وضعیت‌های اجرایی این Task ایجاد می‌کنیم :
static async Task CheckTaskStatus()
{
var task = DummyWeatherProvider.Get("Stockholm");
LogTaskStatus(task.Status);
await task;
LogTaskStatus(task.Status);
}

static void LogTaskStatus(TaskStatus status)
{
Console.WriteLine($"Task Status: {Enum.GetName(typeof(TaskStatus), status)}");
}

ءTaskStatus یک enumeration است، به‌طوری‌که بیانگر وضعیت‌های مختلف یک Task در حال اجرا می‌باشد. برای مثال: WaitingForActivation, Running, RanToCompletion. در کد بالا ابتدا متد را فراخوانی می‌کنیم. سپس منتظر می‌مانیم تا متد اجرا شده، تکمیل شود. در اولین لاگ وضعیت، به WaitingForActivation و در دومین لاگ به RanToCompletion تبدیل میشود. حال‌که با Task ها و نحوه‌ی اجرای فرآیند آن آشنا شدیم، در قسمت بعدی به بررسی ValueTask ها می‌پردازیم.

✨️ء<ValueTask<T چیست؟

همانند تسک ها ، ValueTask هم برای مدیریت وضعیت فرآیند استفاده میشود؛ با این تفاوت که ValueTask ‌ها از نوع struct هستند. به‌طوریکه نحوه‌ی ذخیره سازی آن‌ها در حافظه به نسبت class ‌ها کاملا متفاوت است. از نقطه نظر سرعت، تشخیص دادن اینکه کدامیک باید استفاده شود، باید با توجه به سناریو، بررسی و انتخاب شود؛ چرا که از نظر تخصیص حافظه متفاوت عمل می‌کنند. برای درک بهتر عملکرد ValueTask ‌ها کد زیر را بررسی میکنیم :
public class WeatherService
{
private readonly ConcurrentDictionary<string, Weather> _cache;
public WeatherService()
{
_cache = new();
}

public async Task<Weather> GetWeatherTask(string city)
{
if (!_cache.ContainsKey(city))
{
var weather = await DummyWeatherProvider.Get(city);
_cache.TryAdd(city, weather);
}
return _cache[city];
}

public async ValueTask<Weather> GetWeatherValueTask(string city)
{
if (!_cache.ContainsKey(city))
{
var weather = await DummyWeatherProvider.Get(city);
_cache.TryAdd(city, weather);
}
return _cache[city];
}

کلاس WeatherService شامل یک فیلد private از نوع collection و دو متد است. ما از _cache جهت نگهداری اطلاعاتی که قبلا دریافت شده، استفاده می‌کنیم و به نوعی in-memory cache را پیاده سازی میکنیم. پیاده سازی منطق هر دو متد GetWeatherTask و GetWeatherValueTask کاملا شبیه به هم است؛ به‌طوری‌که اول بررسی میکنیم اطلاعات آب و هوای شهر مورد نظر در _cache وجود دارد یا خیر؟ اگر وجود داشت، اطلاعات به صورت مستقیم برگشت داده می‌شود؛ در غیر این صورت ()DummyWeatherProvider.Get فراخوانی خواهد شد.

در قدم بعدی اطلاعات به‌دست آمده را در cache_ ذخیره می‌کنیم. سپس مقدار ذخیره شده را برگشت میدهیم. در واقع تنها تفاوت دو متد ذکر شده، نوع خروجی آن می‌باشد؛ یکی از Taskو دیگری از ValueTask استفاده می‌کند.


برای مقایسه‌ی مصرف حافظه‌ی این دو روی هر دو متد، Benchmark میگیریم. برای پیاده سازی نیار به کد‌های زیر داریم :
csharp
[MemoryDiagnoser]
public class TaskAndValueTaskBenchmark
{
private readonly WeatherService _weatherService;
public TaskAndValueTaskBenchmark()
{
_weatherService = new();
}

[Benchmark]
[Arguments("Denver")]
public async Task<Weather> TaskBenchmark(string city)
{
return await _weatherService.GetWeatherTask(city);
}

[Benchmark]
[Arguments("London")]
public async ValueTask<Weather> ValueTaskBenchmark(string city)
{
return await _weatherService.GetWeatherValueTask(city);
}
}

نتیجه به دست آمده به شرح زیر است :
TaskBenchmark
{
"Gen0" : "0.0229",
"Allocated" : "144 B"
}

ValueTaskBenchmark
{
"Gen0" : "----",
"Allocated" : "------"
}

با توجه به نتیجه به‌دست آمده، متدی که خروجی ValueTask دارد، حافظه‌ای را تخصیص نداده‌است؛ این دقیقا مزیت مهم ValueTask نسبت به Task می‌باشد.

🛠مزیت <ValueTask<T

به‌دلیل اینکه از نوع struct هستند، بر روی حافظه، در قسمت Stack ذخیره می‌شوند و به صورت خودکار بعد از اینکه نیازی به آنها نباشد، از حافظه حذف می‌شوند . به همین دلیل به شکل قابل توجهی، فشار را از روی GC کاهش می‌دهد .

علاوه بر این، در سناریویی که اکثر کدها به صورت sync اجرا می‌شوند، در این مواقع استفاده از ValueTask، بهتر از Task می‌باشد .

این سری متد GetWeatherValueTask را جهت تشخص اینکه اغلب کدها به صورت sync یا async اجرا می‌شوند، بررسی می‌کنیم. در متد ذکر شده اگر اطلاعات شهر مورد نظر وجود داشته باشد، کار به صورت sync اجرا می‌شود و اگر شهر وجود نداشته باشد، کار به صورت async اجرا می‌شود. با بررسی دقیق‌تر متوجه می‌شویم اکثر مواقع در این متد کار به صورت sync اجرا می‌شود؛ چرا که بعد ازدریافت اطلاعات، مجدد آن را دریافت نمیکند، بلکه از حافظه میخواند (همان cache_ ) .



🔸️محدودیت‌های استفاده از <ValueTask<T

1️⃣ در اینجا تنها یکبار امکان استفاده از await وجود دارد. وقتی یکبار valueTask را await می‌کنیم، بهتر است کار دیگری بر روی آن انجام ندهیم؛ چراکه ممکن است از حافظه پاک شده باشد.

2️⃣ اگر در سناریویی لازم دارید چندین بار await را بر روی valueTask اجرا کنید، لازم است ابتدا آن را به Task تبدیل کنیم. برای این کار متد AsTask را فراخوانی میکنیم (بهتر است صرفا یکبار متد AsTask را فراخوانی کنیم).

3️⃣ نمیتوانیم به یک ValueTask به صورت هم زمان در حالت MultiThreads دسترسی داشته باشیم.

4️⃣ به صورت پیش فرض خروجی عملیات async، نوع Task می‌باشد؛ مگر اینکه اغلب مراحل کار به صورت sync اجرا شود، مانند مثالی که بالاتر اشاره شد.

🔖هشتگ‌ها:
#Task #ValueTask
یک دموی عملی از مهاجرت بدون داون‌تایم با استفاده از Password Hashing 🔐

نیازهای امنیتی به مرور زمان تغییر می‌کنند. چیزی که پنج سال پیش «به اندازه‌ی کافی امن» محسوب می‌شد، ممکن است امروز حتی از یک ممیزی امنیتی هم رد نشود. ⚠️

شما باید به یک الگوریتم مدرن مثل Argon2 یا Bcrypt مهاجرت کنید.
اما اینجا یک مشکل اساسی وجود دارد: هش کردن یک عملیات یک‌طرفه است. شما نمی‌توانید هش‌های قبلی را برعکس کنید تا آن‌ها را «ارتقا» دهید. 🔁

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

در این مقاله می‌خواهم مفهوم zero-downtime migration را به صورت عملی نشان بدهم. 🛠

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

• مهاجرت از فرمت قدیمی به فرمت جدید
• حفظ رفتار قبلی سیستم
• مهاجرت تدریجی داده‌ها
• حذف legacy فقط وقتی کار تمام شد
بیایید شروع کنیم. 🚀

The Naive Approach and Why It Fails
رویکرد ساده‌لوحانه و اینکه چرا شکست می‌خورد

فرض کنید یک سیستم احراز هویت ساده دارید.
می‌خواهید hasher قدیمی PBKDF2 را با یک پیاده‌سازی استاندارد Argon2 جایگزین کنید.

ممکن است فکر کنید:
«فقط پیاده‌سازی جدید را در dependency injection ثبت می‌کنم.»
// Switching from LegacyHasher to ModernHasher
builder.Services.AddSingleton<IPasswordHasher, ModernHasher>();

سناریوی شکست این است:

🔸️کاربران جدید:

ثبت‌نام می‌کنند و بدون مشکل لاگین می‌شوند. پسورد آن‌ها از روز اول با Argon2 هش شده است.

🔸️کاربران قدیمی:

کاربر پسورد درست را وارد می‌کند. سیستم هش قدیمی PBKDF2 را از دیتابیس می‌گیرد. 📦

🔸️فاجعه:

ءModernHasher تلاش می‌کند هش PBKDF2 را verify کند. فوراً fail می‌شود و 401 Unauthorized برمی‌گرداند. 💥

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

The Solution: Migration on Login
راه‌حل: مهاجرت در زمان لاگین 🔄

استراتژی ساده است:
ما دیتابیس را با یک batch job مهاجرت نمی‌دهیم.
ما کاربران را lazy و زمانی که هویت خودشان را ثابت می‌کنند مهاجرت می‌دهیم.

جریان کار به این شکل است:

Attempt 1:
تلاش کن پسورد را با الگوریتم جدید verify کنی. 🥇

Attempt 2 (Fallback):
اگر شکست خورد، بررسی کن ببین الگوریتم قدیمی می‌تواند آن را verify کند یا نه. 🥈

The Migration:
اگر verification با الگوریتم قدیمی موفق بود:

• کاربر را لاگین کن (موفقیت)
• بلافاصله پسورد را با الگوریتم جدید دوباره هش کن 🔐
• رکورد دیتابیس را آپدیت کن 🗄

از این به بعد، لاگین‌های بعدی این کاربر از مسیر استاندارد جدید عبور می‌کنند. 🎯
Implementation with .NET Keyed Services
پیاده‌سازی با استفاده از Keyed Services در NET. 🧩
در NET 8.، مایکروسافت قابلیتی به نام Keyed Services معرفی کرد که برای این سناریو کاملاً ایده‌آل است. این قابلیت به ما اجازه می‌دهد چند پیاده‌سازی از یک interface یکسان را ثبت کنیم و بر اساس نام (کلید) آن‌ها را دریافت کنیم. 🔑

1️⃣ Registering the Services

ما هر دو hasher را در فایل Program.cs ثبت می‌کنیم و به هرکدام یک کلید منحصربه‌فرد می‌دهیم:
// Register the implementations with specific keys
builder.Services.AddKeyedSingleton<IPasswordHasher, Pbdkf2PasswordHasher>("legacy");
builder.Services.AddKeyedSingleton<IPasswordHasher, Argon2PasswordHasher>("modern");

// (Optional) Register the modern one as the default for other services
builder.Services.AddSingleton<IPasswordHasher, Argon2PasswordHasher>();


2️⃣ The Login Command Handler

حالا منطق مهاجرت را پیاده‌سازی می‌کنیم. هر دو hasher را با استفاده از اتریبیوت [FromKeyedServices] تزریق می‌کنیم. 🧪
public class LoginCommandHandler(
IUserRepository userRepository,
[FromKeyedServices("modern")] IPasswordHasher newHasher,
[FromKeyedServices("legacy")] IPasswordHasher legacyHasher)
{
public async Task<AuthenticationResult> Handle(LoginCommand command)
{
var user = await userRepository.GetByEmailAsync(command.Email);
if (user is null)
{
return AuthenticationResult.Fail();
}

// 1. Try the new algorithm first (Happy Path)
if (newHasher.Verify(user.PasswordHash, command.Password))
{
return AuthenticationResult.Success(user);
}

// 2. Fallback: Check if it's a legacy hash
if (legacyHasher.Verify(user.PasswordHash, command.Password))
{
// 3. MIGRATION STEP: Re-hash and save
var newHash = newHasher.Hash(command.Password);

user.UpdatePasswordHash(newHash);
await userRepository.SaveChangesAsync();

return AuthenticationResult.Success(user);
}

return AuthenticationResult.Fail();
}
}

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

Real-World Improvements
بهبودهای دنیای واقعی 🌍

در حالی که پیاده‌سازی بالا کار می‌کند، دو بهبود وجود دارد که آن را production-ready می‌کند.

1️⃣ Algorithm Prefixes
پیشوند الگوریتم‌ها

اتکا به روش «trial and error» برای verify کردن کار می‌کند، اما تمیزتر این است که دقیقاً بدانیم هر هش با چه الگوریتمی ساخته شده است.
الگوریتم‌های استاندارد معمولاً یک prefix دارند
(مثلاً Bcrypt با $2a$ یا $2b$ شروع می‌شود).
می‌توان از این موضوع برای مسیریابی بهینه استفاده کرد:
public bool IsLegacyHash(string hash)
{
// This assumes we're storing a prefix for PBKDF2 hashes. Something to consider.
return hash.StartsWith("pbkdf2$");
}

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

2️⃣ Feature Flags
فلگ‌های ویژگی 🚩

انجام یک write در دیتابیس هنگام لاگین باعث افزایش latency می‌شود. اگر ترافیک بالایی دارید، بهتر است کنترل این rollout را در دست بگیرید.
با قرار دادن منطق مهاجرت پشت یک Feature Flag، می‌توانید در صورت بالا رفتن فشار روی دیتابیس، مرحله‌ی نوشتن را غیرفعال کنید، در حالی که کاربران همچنان از مسیر fallback می‌توانند لاگین کنند.
if (await featureManager.IsEnabledAsync(FeatureFlags.MigratePasswords) &&
legacyHasher.Verify(user.PasswordHash, command.Password))
{
// Perform migration...
}


Finishing the Migration
پایان دادن به مهاجرت 🏁

بعد از مدتی (معمولاً چند ماه)، اکثر اکانت‌های فعال ارتقا پیدا می‌کنند.
سپس می‌توانید یک اسکریپت cleanup اجرا کنید تا هش‌های قدیمی باقی‌مانده را شناسایی کنید و کاربران مربوطه را مجبور کنید در لاگین بعدی پسورد خود را ریست کنند.
در این نقطه می‌توانید موارد زیر را حذف کنید:
• ثبت legacy hasher
• مسیر verification مربوط به legacy
• ءfeature flag
و مهاجرت کامل می‌شود.
جمع‌بندی 🧠

یک ارتقای «به ظاهر ساده» در hashing، در واقع یک data migration است.
این مقاله درباره‌ی الگوی مهاجرت بود، نه درباره‌ی اختراع دوباره‌ی سیستم احراز هویت.
الگوی zero-downtime به این شکل است:

🔹️فرمت جدید برای نوشتن‌های جدید
🔸️پشتیبانی از هر دو فرمت برای خواندن
🔹️مهاجرت تدریجی داده‌های قدیمی (migrate-on-login یک ترفند عالی است)
ُ🔸️قرار دادن پشت feature flag
🔹️حذف legacy وقتی کار تمام شد

با اجازه دادن به هم‌زیستی فرمت قدیمی و جدید برای مدتی، شما یک مهاجرت کاملاً seamless خواهید داشت.
وقتی مانیتورینگ نشان داد که ۹۹٪ کاربران فعال مهاجرت کرده‌اند، می‌توانید کاربران باقی‌مانده روی فرمت قدیمی را شناسایی کنید و در تلاش بعدی، آن‌ها را مجبور به reset پسورد کنید.
امیدوارم مفید بوده باشد!

🔖هشتگ‌ها:
#Security #PasswordHashing #ZeroDowntime #MigrationPatterns
🚫 Stop Using Boolean Parameters
اجتناب از Flag Argument

مثال بد:
user.SetStatus(true);

وقتی این کد را می‌خوانیم، باید مکث کنیم و فکر کنیم:

این true یعنی چی؟ Active؟ Verified؟ Deleted؟

این مسئله به نام Flag Argument شناخته می‌شود.
یعنی پارامتری که مفهومش مبهم است و خواننده را مجبور می‌کند برای فهمیدن هدف متد، به تعریف آن مراجعه کند.

راه‌حل: استفاده از متدهای صریح

به جای کد قبلی از:
user.Activate();

استفاده کنید.

مزایا:

هدف واضح است
زبان طبیعی و قابل فهم
نیاز به توضیح یا comment ندارد

💡 نکته مهم:
کد بسیار بیشتر خوانده می‌شود تا نوشته.
پس هر چه intent کد روشن‌تر باشد، فهم آن سریع‌تر و اشتباهات کمتر می‌شود.
🔖هشتگ‌ها:
#CleanCode #FlagArgument #CodeReadability
Solving Message Ordering from First Principles
حل مسئله‌ی ترتیب پیام‌ها از اصول اولیه 🧠

بیشتر سیستم‌ها به global message ordering نیاز ندارند. 🌍

آن‌ها به چیزی ساده‌تر و در عین حال کاربردی‌تر نیاز دارند:
این‌که رویدادها برای هر aggregate به صورت ترتیبی پردازش شوند. 🔄

برای هر OrderId، برای هر InvoiceId، برای هر CustomerId،یا هر مرزی که برای aggregate خود تعریف کرده‌اید.
می‌توانید این مرز را هرچقدر که لازم دارید گسترده یا محدود کنید. 🎯

این مسئله در ابتدا شبیه یک مشکل در حوزه‌ی eventing به نظر می‌رسد، اما اگر الزامات را تا نتیجه‌ی منطقی‌شان دنبال کنید، در نهایت به یک workflow می‌رسید.
و آن workflow یک نام دارد: Saga 🧩

Domain Events Feel Like the Clean Solution
ءDomain Eventها شبیه راه‌حل تمیز به نظر می‌رسند
ءDomain Eventها جذاب هستند چون از اصول اولیه می‌آیند:

• یک aggregate تغییر وضعیت می‌دهد 🔁
• ءeventهایی منتشر می‌کند که توضیح می‌دهند چه اتفاقی افتاده 📢
• ءhandlerها واکنش نشان می‌دهند و کار مفید انجام می‌دهند ⚙️

و شما یک مدل ذهنی قشنگ هم دارید:
State change → Event → Reaction 🧠➡️📨➡️⚡️

یک مثال معمول:
• OrderPlaced
• PaymentCaptured
• OrderShipped

اما یک مشکل وجود دارد… ⚠️

ءDomain Eventها وقتی می‌خواهید از آن‌ها برای integration استفاده کنید، شکننده می‌شوند.

اگر مستقیماً از داخل transaction رویداد منتشر کنید،دارید درستی بیزینس را به یک side effect غیرقابل اعتماد گره می‌زنید:

🔸️ءtransaction موفق می‌شود ولی publish شکست می‌خورد
🔹️ءpublish موفق می‌شود ولی transaction rollback می‌شود 🔙
🔸️مصرف‌کننده‌ها duplicate پردازش می‌کنند 🔁
🔹️ءretryها باعث reordering می‌شوند 🔀

پس ما مدل را نگه می‌داریم…
اما delivery را مقاوم (hardened) می‌کنیم. 🛡

The Outbox Makes Publishing Reliable (but not ordered)
ءOutbox انتشار رو قابل‌اعتماد می‌کنه (اما مرتب نه) 📦
با Outbox، ما eventهای خروجی رو در همان transaction‌ای ذخیره می‌کنیم که update روی aggregate انجام می‌شه. 🧾

بعد، یک background publisher می‌آید و Outbox رو می‌خونه و eventها رو به یک queue ارسال می‌کنه. 📤

این کار مشکل reliability رو حل می‌کنه:
• اگر transaction commit بشه، event ذخیره شده
• اگر publisher کرش کنه، می‌تونه بعداً ادامه بده 🔄

می‌تونیم با خیال راحت retry کنیم ♻️
حالا انتشار eventها قابل‌اعتماد شده. 👍

اما هنوز ترتیب (ordering) در پردازش eventها تضمین نشده. ⚠️

Competing Consumers Are Great, Until Order Matters
ءCompeting Consumerها عالی‌اند… تا وقتی ترتیب مهم نشه 🚦
به محض این‌که eventها وارد queue می‌شن،
معمولاً با ساده‌ترین راه scale می‌کنیم: competing consumers.

چندین instance از یک queue مشترک مصرف می‌کنن تا throughput بالا بره 📈

این کار جواب می‌ده…
تا زمانی که ترتیب اهمیت پیدا کنه ⛔️

دو event برای یک OrderId ممکنه هم‌زمان پردازش بشن:

• ءConsumer A رویداد PaymentCaptured رو دریافت می‌کنه 💳
• ءConsumer B رویداد OrderPlaced رو دریافت می‌کنه 🛒

ءside effectها خارج از ترتیب اجرا می‌شن 🔀

حتی اگر eventها به ترتیب publish شده باشن،retry و redelivery می‌تونن ترتیب پردازش رو به‌هم بزنن 🔁

و حالا شما با یک باگ ظریف طرف هستید
که فقط زیر load بالا خودش رو نشون می‌ده 🐛🔥

این همون نکته‌ی کلیدیه: queueها کار رو scale می‌کنن، نه invariantهای شما رو 🎯

چیزی که واقعاً می‌خوایم: ترتیب به‌ازای هر Aggregate 🔗

شما به یک خط مرتب برای همه‌چیز نیاز ندارید 🚫
شما به چند خط مرتب مستقل نیاز دارید،
یکی برای هر aggregate 🧵

این معمولاً منطقیه چون:
ءaggregateها از قبل مرزهای consistency رو مشخص می‌کنن 🧱

• ءeventها ذاتاً به ترتیب تولید می‌شن (v1، v2، v3 …) 🔢
• ترتیب «درست» همون timeline خود aggregate هست ⏱️

اگر بتونیم تضمین کنیم که
در هر لحظه فقط یک handler ،eventهای مربوط به یک aggregate خاص رو پردازش کنه، بخش بزرگی از مشکل حل می‌شه

مستقیم‌ترین راه‌حل، که در عین حال ساده‌ترین هم هست:

👉 استفاده از یک consumer واحد برای کل stream

این کار ترتیب رو enforce می‌کنه،
به شرطی که eventها به ترتیب publish شده باشن

اما این راه‌حل یک ایراد واضح داره… ⚠️
A Single Consumer Solves Ordering But Limits Scale
یک Consumer واحد ترتیب رو حل می‌کنه، اما مقیاس‌پذیری رو محدود می‌کنه ⚖️
یک consumer یعنی:
• سقف throughput (فقط یک worker) 🧱
• افزایش latency زیر load بالا 🐢
• ءscaling عمودی می‌شه، نه افقی 📏

حتی اگر eventها سبک باشن، شما به‌صورت مصنوعی کل سیستم رو bottleneck کردید. 🚧

پس ما می‌خوایم:
ترتیب به‌ازای هر aggregate 🔗
مقیاس‌پذیری افقی 🧩
قابلیت اطمینان (Outbox همچنان باقی می‌مونه) 📦

اینجاست که تیم‌ها معمولاً «مرحله‌ی بعدی» رو اختراع می‌کنن. 💡

Publish the Next Message From the Handler
پیام بعدی رو از داخل Handler منتشر کن 📤
اگه competing consumerها ترتیب رو می‌شکنن، یک ایده‌ی طبیعی اینه:
👉 نذاریم queue تصمیم بگیره پیام بعدی چیه، خودمون تصمیم بگیریم.

به‌جای این‌که همه‌ی eventها رو بریزیم تو queue و بذاریم consumerها با هم race کنن،‌می‌ریم سراغ یک مدل زنجیره‌ای:
🔹️یک پیام برای یک aggregate رو handle کن
🔹️وقتی تموم شد، پیام بعدی رو publish کن
🔹️تا step بعدی اجرا بشه

حالا سیستم برای هر aggregate در هر لحظه فقط یک پیام رو پردازش می‌کنه. 🧵

و این لحظه‌ی کلیدیه:

🚨 شما دیگه «event handler» نمی‌سازید.
شما دارید workflow می‌سازید.

و اسم اون workflow چیه؟ 👉 یک Saga.

Congratulations, You Built a Choreographed Saga
تبریک! تو یک Saga کُریوگرافی‌شده ساختی 🎭
یک choreographed saga یعنی:

• هر step به یک event واکنش نشون می‌ده
• یک کاری انجام می‌ده
• ءevent بعدی رو منتشر می‌کنه تا step بعدی شروع بشه
• هیچ coordinator مرکزی وجود نداره.

در عوض، یک زنجیره داریم:
«وقتی X اتفاق افتاد، Y رو انجام بده، بعد Z رو publish کن»

این الگو دقیقاً با نیاز جدیدت فیت می‌شه:
🔹️ترتیب به‌ازای هر aggregate حفظ می‌شه (زنجیره‌ای) 🔗
🔸️می‌تونی روی aggregateهای مختلف scale کنی (چندین زنجیره هم‌زمان) 🧩
🔹️هر step ایزوله و قابل retry هست ♻️

و یک دیسیپلین مفید هم تحمیل می‌کنه:
• «قدم بعدی چیه؟» صریح و شفاف می‌شه
• مرز بین stepها واضح‌تر می‌شه
• می‌تونی کل workflow رو به‌صورت یک sequence مشاهده کنی 👀

اما choreography یک محدودیت داره:
کنترل پخش شده است،
پس track کردن پیشرفت و مدیریت خطاها می‌تونه کثیف بشه. 🧨

پس می‌ریم سراغ قدم نهایی.

If You Want Control, Introduce a State Machine Saga
اگر کنترل می‌خوای، Saga مبتنی بر State Machine بساز 🧠
وقتی workflow مهم می‌شه، معمولاً این‌ها رو می‌خوای:

🔸️یک جای واحد که state فعلی رو بدونه 🗂
🔸️دید روی پیشرفت («کجا گیر کردیم؟») 🔍
🔸️ءtimeout و retry صریح
🔸️اکشن جبرانی وقتی چیزی fail می‌شه 🔄

اینجاست که از choreography می‌ری به سمت orchestration
با استفاده از یک state machine saga:

🔹️ءsaga وضعیت workflow رو نگه می‌داره
🔹️ءeventها transitionها رو جلو می‌برن
🔹️ءsaga تصمیم می‌گیره پیام بعدی چی باشه

تو این مدل، کنترل و observability رو به دست میاری. 🎛

و نکته‌ی مهم:
🔸️این جای Outbox رو نمی‌گیره.
🔸️📦 تو هنوز به انتشار قابل‌اعتماد نیاز داری.
🔸️تو فقط workflow رو explicit کردی.
Broker Support Helps with Ordering, not Correctness
پشتیبانی Broker به ترتیب کمک می‌کنه، نه به درستی سیستم ⚖️
بد نیست این نکته رو صریح بگیم:
همیشه لازم نیست همه‌ی این‌ها رو خودت بسازی.

خیلی از message brokerهای معروف، primitiveهای فنی برای پردازش مرتب به‌ازای هر key (یعنی aggregate ID) دارن:

🔹️ءAmazon SQS FIFO message groups (به‌ازای هر key)
🔸️ءAzure Service Bus sessions (به‌ازای هر key)
🔹️ءKafka Partitions در یک log (key → partition → ordered stream)
🔸️ءRabbitMQ با semantics مدل "single active consumer" (به‌ازای هر queue)

این قابلیت‌ها رایج‌ترین حالت خراب شدن competing consumerها رو حذف می‌کنن:
پردازش هم‌زمان پیام‌ها برای یک aggregate واحد. 🚫

اما حتی با ترتیب بی‌نقص به‌ازای هر aggregate،باز هم برای درست کار کردن سیستم به الگوهای دیگه نیاز داری:

🔹️ءOutbox برای انتشار قابل‌اعتماد (ترتیب بی‌فایده است اگر eventها گم بشن) 📦
🔸️ءConsumerهای idempotent / الگوی Inbox چون retry و duplicate همچنان اتفاق می‌افته ♻️
🔹️مرزهای consistency (چی رو می‌شه داخل transaction انجام داد و چی رو نه) 🔒
🔸️ءTimeout + compensation وقتی «دنباله‌ی مرتب» در واقع یک workflow تجاریه که ممکنه نیمه‌کاره fail بشه

پس ordering در سطح broker یک پایه‌ی عالیه.
پیچیدگی تصادفی رو کم می‌کنه.
اما نیاز به مدل‌سازی صریح کارهای طولانی‌مدت رو حذف نمی‌کنه، وقتی بیزنس بهش نیاز داره. 🧩

جمع‌بندی 🧠

اگه مسئله رو از first principles دنبال کنی:

🔹️ءaggregateها مرزی هستن که ترتیب داخلشون مهمه
🔸️ءOutbox انتشار event رو قابل‌اعتماد می‌کنه
🔹️ءcompeting consumerها ترتیب per-aggregate رو می‌شکنن
🔸️یک consumer واحد ترتیب رو برمی‌گردونه ولی throughput رو محدود می‌کنه
🔹️انتشار «پیام بعدی» باعث پیشرفت ترتیبی برای هر aggregate می‌شه
🔸️اون پیشرفت ترتیبی یک Saga است
(اول choreographed، و وقتی کنترل خواستی state machine)

پس تو به‌صورت تصادفی چیزی رو دوباره اختراع نکردی.

تو کشف کردی که:
«پردازش مرتب به‌ازای هر aggregate در مقیاس بالا»
یک feature از queue نیست.

این یک workflow است.
و Saga مدلی است که ما برای workflow در سیستم‌های توزیع‌شده استفاده می‌کنیم. 🌐

وقتی این رو ببینی،
دیگه با queueها سر ordering دعوا نمی‌کنی.

تو workflowی که بیزنس واقعاً نیاز داره رو طراحی می‌کنی. 🎯
امیدوارم مفید بوده باشه! 🚀
ASP.NET Core Middleware 🧩

ءMiddleware نرم‌افزاری است که در یک pipeline برنامه کنار هم قرار می‌گیرد تا درخواست‌ها و پاسخ‌ها را مدیریت کند. هر کامپوننت:
تصمیم می‌گیرد که آیا درخواست را به کامپوننت بعدی در pipeline ارسال کند یا نه. 🔀

می‌تواند قبل و بعد از کامپوننت بعدی، پردازش انجام دهد. ⏱️

برای ساختن request pipeline از Request Delegate‌ها استفاده می‌شود. این delegateها هر درخواست HTTP را مدیریت می‌کنند.

ءRequest delegateها با استفاده از متدهای extension زیر پیکربندی می‌شوند:
• Run
• Map
• Use

یک request delegate می‌تواند:
• به‌صورت in-line و با یک متد anonymous تعریف شود (که به آن in-line middleware می‌گویند)،
• یا در قالب یک کلاس قابل استفاده مجدد تعریف شود.

این کلاس‌های reusable و متدهای anonymous در واقع همان middleware یا middleware component هستند.
هر middleware در pipeline مسئول است یا کامپوننت بعدی را فراخوانی کند، یا pipeline را short-circuit کند.

وقتی یک middleware pipeline را short-circuit می‌کند، به آن terminal middleware گفته می‌شود،
چون جلوی ادامه‌ی پردازش توسط middlewareهای بعدی را می‌گیرد.

ساخت middleware pipeline با WebApplication 🛠

ءrequest pipeline در ASP.NET Core شامل یک دنباله از request delegateها است که یکی پس از دیگری فراخوانی می‌شوند.

⚡️ ASP.NET Core Request Delegates و Pipeline

هر delegate می‌تواند قبل و بعد از delegate بعدی عملیات انجام دهد.

توصیه: exception-handling delegateها را اوایل pipeline قرار دهید تا بتوانند خطاهای رخ داده در مراحل بعدی را بگیرند.

ساده‌ترین اپلیکیشن ASP.NET Core 🟢

در ساده‌ترین حالت، یک single request delegate تعریف می‌شود که همه درخواست‌ها را هندل می‌کند.

در این حالت، pipeline واقعی وجود ندارد.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Run(async context =>
{
await context.Response.WriteAsync("Hello world!");
});

app.Run();


زنجیره کردن چند delegate با Use 🔗

ءnext نمایانگر delegate بعدی در pipeline است.
می‌توان pipeline را با عدم فراخوانی next کوتاه کرد (Short-circuit).

معمولاً می‌توان کارها را قبل و بعد از next انجام داد:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
// کاری که می‌تواند Response بنویسد
await next.Invoke();
// کارهای logging یا دیگر عملیات غیر از نوشتن Response
});

app.Run(async context =>
{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});

app.Run();


Short-Circuiting Pipeline ⚡️

اگر یک delegate درخواست را به delegate بعدی ندهد، pipeline short-circuit می‌شود.

کاربرد: جلوگیری از کار غیرضروری

مثال: Static File Middleware می‌تواند terminal middleware باشد و پس از پردازش فایل، pipeline را کوتاه کند.

توجه: Middlewareهایی که قبل از terminal middleware آمده‌اند، هنوز بعد از next.Invoke کد اجرا می‌کنند.

⚠️ هشدار مهم

بعد یا هنگام ارسال Response به کلاینت، next.Invoke را فراخوانی نکنید!

تغییر header یا status code بعد از شروع Response → Exception

نوشتن به body بعد از next ممکن است:
باعث violation پروتکل شود (مثلاً نوشتن بیشتر از Content-Length)

فرمت body خراب شود (مثلاً HTML footer در فایل CSS)

نکته: HasStarted می‌تواند کمک کند بررسی کنید که آیا headers یا body قبلاً ارسال شده‌اند یا نه.

Run delegates🏁

ءRun delegateها پارامتر next دریافت نمی‌کنند.
اولین Run delegate همیشه terminal است و pipeline را خاتمه می‌دهد.
ءRun یک convention است. بعضی از middleware componentها ممکن است متدهایی مثل Run[Middleware] ارائه دهند که در انتهای pipeline اجرا می‌شوند:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    // Do work that can write to the Response.
    await next.Invoke();
    // Do logging or other work that doesn't write to the Response.
});

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from 2nd delegate.");
});

app.Run();

در مثال بالا، Run delegate عبارت "Hello from 2nd delegate." را در response می‌نویسد و سپس pipeline را خاتمه می‌دهد.
اگر بعد از Run delegate، یک Use یا Run دیگر اضافه شود، دیگر فراخوانی نخواهد شد. 🛑
ترتیب middlewareها 🧱

در این دیاگرام کل pipeline پردازش درخواست در ASP.NET Core MVC و Razor Pages را نشان می‌دهد.
می‌توانید ببینید که در یک اپلیکیشن معمولی، middlewareهای موجود چه ترتیبی دارند و middlewareهای سفارشی کجا اضافه می‌شوند.
شما کنترل کامل دارید که middlewareهای موجود را جابه‌جا کنید یا middleware جدید تزریق کنید، متناسب با سناریوهای خودتان. 🔧

ءEndpoint middleware در دیاگرام بالا، filter pipeline مربوط به نوع اپلیکیشن (MVC یا Razor Pages) را اجرا می‌کند.

ءRouting middleware در دیاگرام بالا بعد از Static Files نشان داده شده است.
این همان ترتیبی است که قالب‌های پیش‌فرض پروژه با فراخوانی صریح app.UseRouting پیاده‌سازی می‌کنند.
اگر app.UseRouting را صدا نزنید، Routing middleware به‌صورت پیش‌فرض در ابتدای pipeline اجرا می‌شود.
برای اطلاعات بیشتر، بخش Routing را ببینید.

ترتیبی که middlewareها در فایل Program.cs اضافه می‌شوند، دقیقاً ترتیب اجرای آن‌ها روی request و ترتیب معکوس برای response را مشخص می‌کند.
این ترتیب برای امنیت، کارایی و عملکرد صحیح کاملاً حیاتی است. ⚠️

ترتیب پیشنهادی middlewareهای امنیتی

کد زیر در Program.cs middlewareهای مربوط به امنیت را در ترتیب توصیه‌شده اضافه می‌کند:
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMiddleware.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();

app.UseRouting();
// app.UseRateLimiter();
// app.UseRequestLocalization();
// app.UseCors();

app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();

در کد بالا: middlewareهایی که هنگام ساخت یک وب‌اپ با حساب کاربری فردی اضافه نمی‌شوند، کامنت شده‌اند.

همه‌ی middlewareها دقیقاً در همین ترتیب ظاهر نمی‌شوند، اما بسیاری از آن‌ها همین‌طور هستند. برای مثال:

قوانین مهم ترتیب middlewareها 📌

ءUseCors، UseAuthentication و UseAuthorization باید دقیقاً به همین ترتیب باشند.

ءUseCors در حال حاضر باید قبل از UseResponseCaching قرار بگیرد. این الزام در GitHub issue شماره dotnet/aspnetcore #23218 توضیح داده شده است.

ءUseRequestLocalization باید قبل از هر middlewareای باشد که ممکن است culture درخواست را بررسی کند، مثلاً ()app.UseStaticFiles

وقتی rate limiting به‌صورت endpoint-specific استفاده می‌شود (مثلاً با [EnableRateLimiting])، باید UseRateLimiter بعد از UseRouting صدا زده شود.

اگر فقط global limiterها استفاده شوند، می‌توان UseRateLimiter را قبل از UseRouting هم صدا زد.

ترتیب‌های جایگزین در برخی سناریوها

در بعضی سناریوها ترتیب middleware متفاوت است.
مثلاً ترتیب caching و compression وابسته به سناریو است و چندین ترتیب معتبر وجود دارد. برای مثال:
app.UseResponseCaching();
app.UseResponseCompression();

در این حالت، مصرف CPU ممکن است کاهش پیدا کند چون response فشرده‌شده cache می‌شود،
اما ممکن است چند نسخه‌ی مختلف از یک resource با الگوریتم‌های فشرده‌سازی متفاوت مثل Gzip یا Brotli در cache ذخیره شوند. 📦
⚡️ ترتیب Middleware در ASP.NET Core و نکات مهم

کش کردن و فشرده‌سازی فایل‌های استاتیک 📦💨

برای اینکه فایل‌های استاتیک Cacheable و Compressed شوند، ترتیب زیر پیشنهاد می‌شود:
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();

ءProgram.cs: Middlewareهای رایج در سناریوهای مختلف

1️⃣ مدیریت Exception/Error

🔸️Development:
• UseDeveloperExceptionPage() → نمایش خطاهای runtime اپلیکیشن

• UseDatabaseErrorPage() → نمایش خطاهای database

🔹️Production:
• UseExceptionHandler() → گرفتن خطاهای ایجاد شده در middlewareهای بعدی

• UseHsts() → اضافه کردن هدر Strict-Transport-Security

• UseHttpsRedirection() → ریدایرکت HTTP به HTTPS

2️⃣ فایل‌های استاتیک و Policyها

• UseStaticFiles() → پاسخ به درخواست‌های فایل‌های استاتیک و کوتاه کردن pipeline

⚠️ توجه: هیچ بررسی authorization انجام نمی‌دهد. تمام فایل‌ها، از جمله wwwroot، عمومی هستند.

• UseCookiePolicy() → رعایت GDPR

3️⃣ Routing و Security

• UseRouting() → مسیردهی درخواست‌ها
• UseAuthentication() → بررسی هویت کاربر
• UseAuthorization() → اجازه دسترسی به منابع امن

⚠️توجه: Authentication short-circuit نمی‌کند؛ فقط هویت را بررسی می‌کند. مجوز دسترسی بعد از انتخاب Controller یا Razor Page اعمال می‌شود.

4️⃣ Session و Endpoint

• UseSession() → مدیریت وضعیت session
اگر از session استفاده می‌کنید، حتماً بعد از Cookie Policy و قبل از MVC Middleware قرار دهید.

• MapRazorPages() → اضافه کردن Endpointهای Razor Pages به pipeline

مثال کد کامل Program.cs
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();


نکات کلیدی 📝

• ءUseExceptionHandler → اولین middleware است، پس همه Exceptionهای بعدی را می‌گیرد.

• ءStatic File Middleware → زود فراخوانی می‌شود تا درخواست‌ها را هندل کرده و pipeline را کوتاه کند.

• ءSecurity → فایل‌های استاتیک بدون بررسی authorization هستند. برای ایمن‌سازی، به Secure Static Files در ASP.NET Core
مراجعه کنید.

Authentication vs Authorization

• Authentication → بررسی هویت
• Authorization → بعد از انتخاب Controller/Razor Page اعمال می‌شود

ترتیب فشرده‌سازی فایل‌ها

اگر Static Files قبل از ()UseResponseCompression بیاید → فایل‌های استاتیک فشرده نمی‌شوند، اما پاسخ Razor Pages فشرده می‌شود:
app.UseStaticFiles();
app.UseRouting();
app.UseResponseCompression();
app.MapRazorPages();

🔖هشتگ‌ها:
#ASPNETCore #Middleware #Pipeline #DotNe #RequestPipeline #CSharp
چگونه یک پروژه‌ی جدید NET. را در سال 2026 شروع کنیم 🚀

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

بیایید شروع کنیم. 🔽

1️⃣ Directory.Build.props - Set Project-Wide Standards

فایل Directory.Build.props – تنظیم استانداردهای سراسری پروژه 🧱

هر solution در NET. باید با یک فایل Directory.Build.props شروع شود.
این فایل تنظیمات سراسری پروژه را تعریف می‌کند که روی تمام پروژه‌های داخل solution اعمال می‌شود.

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

ءDirectory.Build.props این مشکل را با متمرکز کردن تنظیمات در یک مکان حل می‌کند.
این فایل را در همان دایرکتوری فایل sln. ایجاد می‌کنید و MSBuild به‌صورت خودکار آن را روی همه‌ی پروژه‌ها اعمال می‌کند.

این کانفیگی است که من برای هر پروژه‌ی جدید استفاده می‌کنم:
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>All</AnalysisMode>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>

در صورت نیاز، می‌توانید Directory.Build.props را در هر سطحی از ساختار دایرکتوری پروژه قرار دهید و تنظیمات را در آن سطح override کنید.

توضیح هر تنظیم:
🔸️Nullable:

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

🔹️ImplicitUsings:

ءnamespaceهای رایج را به‌صورت خودکار در هر فایل اضافه می‌کند. دیگر نیازی نیست بنویسید: using System; یا using System.Linq;

🔸️AnalysisLevel:

سطح آنالیز کد را روی آخرین نسخه تنظیم می‌کند. جدیدترین بررسی‌های کیفیت کد از سمت مایکروسافت فعال می‌شوند.

🔹️AnalysisMode:

تمام قوانین code analysis را فعال می‌کند و کامل‌ترین بازخورد ممکن درباره‌ی کیفیت کد را می‌دهد.

🔸️TreatWarningsAsErrors:

اگر هر هشداری وجود داشته باشد، کامپایل متوقف می‌شود. این کار شما را مجبور می‌کند مشکلات را همان لحظه حل کنید و اجازه ندهید انباشته شوند. 🚨

🔹️CodeAnalysisTreatWarningsAsErrors:

همین سخت‌گیری را روی هشدارهای code analysis هم اعمال می‌کند.

🔸️EnforceCodeStyleInBuild:

بررسی style کد را در زمان build اجرا می‌کند، نه فقط داخل IDE. یعنی در CI/CD هم تخلفات استایل شناسایی می‌شوند.

این تنظیمات یک پایه‌ی بسیار قوی برای کیفیت کد ایجاد می‌کنند.
مشکلات را زود شناسایی می‌کنند و یکپارچگی را در کل solution حفظ می‌کنند. 🧠

2️⃣ Add Static Code Analysis Packages

اضافه کردن پکیج‌های Static Code Analysis 🔍

کیفیت کد چیزی است که باید از روز اول به آن اهمیت بدهید.
پیروی از best practiceها بسیار راحت‌تر از این است که بعداً بخواهید آن‌ها را اصلاح کنید.
برای این کار از static code analysis استفاده می‌کنیم. static analyzerها کد شما را بدون اجرا بررسی می‌کنند.
آن‌ها اشتباهات رایج را پیدا می‌کنند، استانداردهای کدنویسی را enforce می‌کنند و باگ‌های احتمالی را قبل از رسیدن به production شناسایی می‌کنند.

این analyzerها هنگام کامپایل اجرا می‌شوند، پس هم در IDE و هم در CI/CD بازخورد فوری می‌گیرید. ⚡️
این پکیج‌ها را به فایل Directory.Build.props اضافه کنید:
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>All</AnalysisMode>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.257">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.16.0.128591">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.analyzers" Version="1.26.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

نقش هر analyzer:
🔸️SonarAnalyzer.CSharp:

روی کیفیت کد، آسیب‌پذیری‌های امنیتی و code smell تمرکز دارد. متدهای پیچیده، کدهای تکراری و باگ‌های احتمالی را شناسایی می‌کند.

🔹️Meziantou.Analyzer:

مشکلات performance، ضعف‌های امنیتی و استفاده‌ی نادرست از APIها را پیدا می‌کند. صدها قانون برای async/await، LINQ، string handling و موارد دیگر دارد.

🔸️Roslynator.Analyzers:

پیشنهادهای refactoring و تحلیل کد ارائه می‌دهد و کمک می‌کند کد تمیزتر و idiomaticتری در #C بنویسید.

🔹️xunit.analyzers:

درست نوشتن تست‌ها را enforce می‌کند و اشتباهات رایج مثل نبود assertion یا attribute اشتباه را شناسایی می‌کند.

تنظیم IncludeAssets باعث می‌شود این analyzerها فقط در زمان کامپایل اجرا شوند و وارد خروجی نهایی برنامه نشوند. 📦

با فعال بودن TreatWarningsAsErrors، اگر هر analyzer مشکلی پیدا کند، build شکست می‌خورد.
شاید سخت‌گیرانه به نظر برسد، اما جلوی انباشته شدن technical debt را می‌گیرد.

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

3️⃣ Enforce Coding Standards
اعمال استانداردهای کدنویسی 📏

ءAnalyzerها مشکلات را شناسایی می‌کنند، اما شما همچنین باید مشخص کنید کدام قوانین برای تیم شما مهم هستند و میزان سخت‌گیری آن‌ها چقدر باشد.

فایل editorconfig. استانداردهای کدنویسی را تعریف می‌کند و سطح severity هر قانون analyzer را مشخص می‌کند. شما می‌توانید قوانین را به‌صورت error، warning، suggestion علامت‌گذاری کنید یا کاملاً غیرفعالشان کنید.

این فایل را در همان دایرکتوری فایل sln. قرار دهید. همه‌ی توسعه‌دهندگان تیم شما، بدون توجه به اینکه از چه IDEای استفاده می‌کنند، به‌صورت خودکار از یک مجموعه قوانین یکسان پیروی خواهند کرد. 🤝

یک فایل پایه‌ی editorconfig. برای شروع:
[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

[*.cs]
# Nullable reference types
dotnet_diagnostic.CS8600.severity = error
dotnet_diagnostic.CS8601.severity = error
dotnet_diagnostic.CS8602.severity = error
dotnet_diagnostic.CS8603.severity = error
dotnet_diagnostic.CS8604.severity = error

# Code style rules
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning

# Naming conventions
dotnet_naming_rule.interface_should_begin_with_i.severity = error
dotnet_naming_rule.interface_should_begin_with_i.symbols = interface
dotnet_naming_rule.interface_should_begin_with_i.style = begins_with_i

dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.capitalization = pascal_case

# Async methods should end with Async
dotnet_naming_rule.async_methods_end_in_async.severity = error
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
dotnet_naming_rule.async_methods_end_in_async.style = end_in_async

dotnet_naming_symbols.any_async_methods.applicable_kinds = method
dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
dotnet_naming_symbols.any_async_methods.required_modifiers = async

dotnet_naming_style.end_in_async.required_suffix = Async
dotnet_naming_style.end_in_async.capitalization = pascal_case

شما می‌توانید این فایل را مطابق سلیقه‌ی تیم خودتان شخصی‌سازی کنید. نکته‌ی کلیدی این است که از ابتدا روی استانداردها به توافق برسید و آن‌ها را به‌صورت خودکار enforce کنید. 🔒

با استفاده از editorconfig. ، کد reviewها سریع‌تر می‌شوند چون دیگر لازم نیست توسعه‌دهندگان درباره‌ی فرمت‌بندی یا naming convention بحث کنند. ابزارها این قوانین را به‌صورت خودکار اعمال می‌کنند. ⚙️

4️⃣ Centralize Package Management 📦

با بزرگ‌تر شدن solution، مدیریت نسخه‌ی پکیج‌های NuGet در چندین پروژه سخت می‌شود. پروژه‌های مختلف در نهایت از نسخه‌های متفاوت یک پکیج استفاده می‌کنند که می‌تواند باعث مشکلات سازگاری شود و آپدیت‌ها را سخت‌تر کند. Central Package Management (CPM) این مشکل را با مدیریت همه‌ی نسخه‌های پکیج در یک مکان حل می‌کند.

شما باید یک فایل Directory.Packages.props بسازید که تمام نسخه‌های پکیج برای solution را تعریف می‌کند. این فایل را در همان دایرکتوری .sln قرار دهید:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<!-- Web -->
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />

<!-- Database -->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />

<!-- Testing -->
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<!-- Code Analysis -->
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.257" />
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.16.0.128591" />
<PackageVersion Include="Roslynator.Analyzers" Version="4.14.1" />
<PackageVersion Include="xunit.analyzers" Version="1.26.0" />
</ItemGroup>
</Project>

برای فعال شدن CPM باید property زیر را در این فایل اضافه کنید:ManagePackageVersionsCentrally

وقتی CPM فعال باشد، فایل‌های پروژه پکیج‌ها را بدون مشخص کردن نسخه ریفرنس می‌کنند:
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>

در صورت نیاز، می‌توانید نسخه‌ی یک پکیج را در یک پروژه‌ی خاص override کنید:
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
</ItemGroup>
5️⃣ساده‌سازی توسعه‌ی لوکال با Aspire 🚀

راه‌اندازی محیط توسعه‌ی لوکال معمولاً آزاردهنده است. توسعه‌دهنده‌ها ساعت‌ها زمان صرف نصب دیتابیس‌ها، تنظیم connection stringها و هماهنگ کردن وابستگی‌ها با هم می‌کنند. 😵‍💫

حتی با وجود Docker، باز هم مقدار زیادی تنظیمات دستی وجود دارد؛ از جمله volumeها و کانفیگ هر کانتینر.

ءNET Aspire. این مشکل را با ارائه‌ی یک روش یکپارچه برای مدیریت وابستگی‌ها، پیکربندی و استقرار (deployment) حل می‌کند.

ءAspire یک application framework برای ساخت اپلیکیشن‌های cloud-native، قابل مشاهده (observable) و آماده‌ی production با NET. است. این فریم‌ورک به‌صورت پیش‌فرض موارد زیر را هندل می‌کند:
• Service discovery
• Configuration
• Health checks
• Telemetry 📊
از نسخه‌ی 13 به بعد، NET Aspire. به Aspire تغییر برند داد و حالا یک پلتفرم چندزبانه (multi-language) برای ساخت اپلیکیشن است.

ءAspire دو نوع پروژه‌ی جدید به solution شما اضافه می‌کند:

🔸️ءAppHost: ارکستریتوری که معماری اپلیکیشن و وابستگی‌ها را تعریف می‌کند.

🔹️ءServiceDefaults: تنظیمات مشترک و observability که روی همه‌ی سرویس‌ها اعمال می‌شود.

نحوه‌ی اضافه کردن Aspire به پروژه‌ی جدید

مرحله 1️⃣: نصب templateهای Aspire
dotnet new install Aspire.ProjectTemplates

مرحله 2️⃣: نصب Aspire CLI
dotnet tool install --global aspire.cli

مرحله 3️⃣: اضافه کردن Aspire به solution از طریق Visual Studio یا Rider، یا استفاده از CLI برای ساخت یک پروژه‌ی جدید Aspire.

مرحله 4️⃣: تعریف وابستگی‌ها در پروژه‌ی AppHost
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");
var postgres = builder.AddPostgres("postgres");

builder
.AddProject<Projects.Products_Api>("products-api");
.WithReference(cache)
.WithReference(postgres);

builder.Build().Run();

مرحله 5️⃣: استفاده از connection stringها در پروژه‌ی API
var postgresConnection = configuration.GetConnectionString("postgres");
var redisConnection = configuration.GetConnectionString("cache");

ءAspire به‌صورت خودکار connection stringها را از طریق environment variableها تزریق می‌کند. نیازی نیست آن‌ها را دستی داخل appsettings.json تعریف کنید. ⚡️

با یکپارچگی Docker، وقتی پروژه‌ی AppHost را اجرا می‌کنید، Aspire تمام وابستگی‌ها را به‌صورت خودکار بالا می‌آورد. PostgreSQL، Redis و هر سرویس دیگری داخل کانتینر اجرا می‌شوند و Aspire چرخه‌ی عمر آن‌ها را مدیریت می‌کند. 🐳

داشبورد Aspire در مرورگر باز می‌شود و همه‌ی سرویس‌های در حال اجرا، وضعیت سلامت، لاگ‌ها و distributed traceها را نمایش می‌دهد. 📈

مزایای کلیدی استفاده از Aspire

🔸️Simplified local development:
اجرای تمام سرویس‌ها و دیتابیس‌ها با یک دستور واحد.

🔹️Built-in observability:
یکپارچگی آماده با OpenTelemetry برای لاگ، متریک و تریس.

🔸️Service discovery:
سرویس‌ها بدون URLهای هاردکد شده همدیگر را پیدا می‌کنند.

🔹️Configuration management:
پیکربندی متمرکز برای لوکال و کلاود.

🔸️Easy deployment:
استقرار کل سیستم روی Docker، Azure یا AWS با کمترین تنظیمات.

🔹️Consistent developer experience:
اعضای جدید تیم می‌توانند کل سیستم را در چند دقیقه اجرا کنند. 🎯
6️⃣ راه‌اندازی OpenTelemetry 🔍

درک این‌که داخل اپلیکیشن شما دقیقاً چه اتفاقی می‌افتد، حیاتی است. OpenTelemetry یک روش استاندارد برای جمع‌آوری و تحلیل داده‌های telemetry از اپلیکیشن‌ها فراهم می‌کند و به شما دید (visibility) دقیقی نسبت به رفتار سیستم می‌دهد. 👁

هر اپلیکیشن وابستگی‌های خارجی دارد؛ مثل دیتابیس‌ها، cacheها، APIها و سایر سرویس‌ها. مانیتور کردن این وابستگی‌ها برای درک عملکرد اپلیکیشن و نحوه‌ی تعامل آن با بقیه‌ی سیستم ضروری است. ⚙️

ءOpenTelemetry سه نوع داده از اپلیکیشن جمع‌آوری می‌کند:

• ءLogs توضیح می‌دهند چه اتفاقی افتاده است. 📝
• ءTraces نشان می‌دهند عملیات کجا انجام شده و چقدر زمان برده است. ⏱️
• ءMetrics نشان می‌دهند رویدادها با چه فرکانسی رخ می‌دهند. 📊

وقتی از Aspire استفاده می‌کنید، OpenTelemetry از قبل برای شما پیکربندی شده است. پروژه‌ی ServiceDefaults شامل تنظیمات observability است:
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});

builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation();
});

builder.AddOpenTelemetryExporters();
return builder;
}

ءAspire به‌صورت خودکار OpenTelemetry را طوری تنظیم می‌کند که logs، metrics و traces را به داشبورد Aspire ارسال کند.
اما Aspire داده‌های OpenTelemetry و داشبورد را ذخیره (persist) نمی‌کند. در محیط production، به یک راه‌حل قوی‌تر برای ذخیره و تحلیل داده‌های telemetry نیاز دارید. 🏗

می‌توانید exporterهای اضافی تنظیم کنید تا داده‌ها به ابزارهایی مثل Jaeger، Seq، Grafana یا سایر ابزارهای observability ارسال شوند. 📡

به‌صورت پیش‌فرض، OpenTelemetry فقط برای ASP.NET Core و HttpClient فعال است. برای وابستگی‌های اضافی مثل PostgreSQL یا Redis، باید پکیج‌های instrumentation را نصب کنید:
dotnet add package Npgsql.OpenTelemetry
dotnet add package OpenTelemetry.Instrumentation.StackExchangeRedis

سپس آن‌ها را در ServiceDefaults رجیستر کنید:
tracing.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddNpgsql()
.AddRedisInstrumentation();

داشبورد Aspire، distributed traceها را در تمام سرویس‌ها نمایش می‌دهد. می‌توانید دقیقاً ببینید چه کوئری‌هایی روی دیتابیس اجرا شده‌اند، چقدر طول کشیده‌اند و bottleneckها کجا هستند. 🧠

شروع observability از همان روز اول به شما کمک می‌کند ویژگی‌های عملکردی سیستم را زودتر درک کنید. می‌توانید کوئری‌های کند را شناسایی کنید، API callها را بهینه کنید و مشکلات را قبل از رسیدن به production رفع کنید. 🚀
7️⃣ خودکارسازی Build و Test با GitHub Actions 🤖

توسعه‌ی مدرن نرم‌افزار نیازمند pipelineهای خودکار است. شما نمی‌توانید به استقرار دستی یا رد کردن تست‌های خودکار متکی باشید. 🚫

یک pipeline خوب CI/CD تضمین می‌کند که هر تغییر در کد به‌صورت یکسان build، test و deploy شود. این کار باگ‌ها را قبل از رسیدن به production شناسایی می‌کند و سرعت تحویل شما را افزایش می‌دهد. ⚡️

بدون pipeline مناسب CI/CD، زمان زیادی صرف کارهای دستی می‌کنید و ریسک خطای انسانی به‌شدت افزایش پیدا می‌کند.

برای توسعه‌دهندگان NET. که از GitHub استفاده می‌کنند، GitHub Actions انتخاب طبیعی است. این ابزار به‌صورت یکپارچه با ریپازیتوری شما کار می‌کند و هر چیزی که برای build و test اپلیکیشن‌های NET. نیاز دارید را فراهم می‌کند. 🧩

یک فایل به نام github/workflows/ci.yml. در ریپازیتوری خود بسازید:
name: BUILD_AND_TEST

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
dotnet-version: [ '10.0.x' ]

steps:
- uses: actions/checkout@v3

- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ matrix.dotnet-version }}

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore

- name: Test
run: dotnet test --no-build --no-restore --verbosity normal

- name: Build Docker image with Aspire
if: ${{ success() }}
run: |
dotnet tool install --global aspire.cli
aspire publish -o docker-compose-artifacts
docker compose -f docker-compose-artifacts/docker-compose.yaml build

این pipeline روی هر push به شاخه‌ی main و روی هر pull request اجرا می‌شود. وابستگی‌ها را restore می‌کند، solution را build می‌کند، تست‌ها را اجرا می‌کند و با استفاده از Aspire یک Docker image می‌سازد. 🐳

ءAspire CLI یک فایل Docker Compose تولید می‌کند که شامل تمام سرویس‌ها و وابستگی‌های آن‌هاست. سپس pipeline ایمیج‌های Docker را برای استقرار build می‌کند. 🏗

بهترین روش‌ها برای CI/CD:

• Keep pipelines fast:
هدف زیر ۱۰ دقیقه برای CI builds باشد. ⏱️
• Run tests on every pull request:
مشکلات را قبل از merge شدن به main شناسایی کنید. 🔍
• Store secrets securely:
از GitHub Secrets برای داده‌های حساس مثل API key و connection string استفاده کنید. هرگز secret را در ریپو commit نکنید. 🔐
• Fail fast:
اگر تست‌ها fail شدند، pipeline را فوراً متوقف کنید. برای کد خراب Docker image نسازید.
• Use caching:
پکیج‌های NuGet و artifactهای build را cache کنید تا اجراهای بعدی سریع‌تر شوند. ⚡️

8️⃣ قالب پروژه برای شروع 🚀

راه‌اندازی یک پروژه‌ی جدید NET. با مراحل بالا سخت نیست. اما چیزهای بسیار بیشتری وجود دارد که باید در پروژه تنظیم شوند، مثل:

• Code Structure: Clean Architecture / Vertical Slices / N-Layered
• Authentication و Authorization
• ASP .NET Core Identity
• JWT Claims و Refresh Token
• Logging
• Result Pattern
• Architecture Tests
• Unit Tests و Integration Tests
• ساختار endpointهای WebApi
• یکپارچه‌سازی با Jaeger و Seq

جمع‌بندی 🧠

شروع یک پروژه‌ی جدید NET. با فونداسیون درست، در بلندمدت زمان ذخیره می‌کند و از ایجاد technical debt جلوگیری می‌کند.

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

امیدوارم این مقاله برایتان مفید بوده باشد👋

🔖هشتگ‌ها:
#DotNet #SoftwareEngineering
#OpenTelemetry #Aspire #DeveloperExperience