C# Geeks (.NET) – Telegram
Forwarded from tech-afternoon (Amin Mesbahi)
📕 پیشنهاد کتاب: Crafting Engineering Strategy

کمتر از ۲ هفته دیگه (۲۵ نوامبر ۲۰۲۵)، کتاب

Crafting Engineering Strategy:
How Thoughtful Decisions Solve Complex Problems


یا: طراحی استراتژی مهندسی: چگونه تصمیم‌گیری‌های سنجیده مسائل پیچیده رو حل می‌کنن؛ منتشر می‌شه. توی معرفی اولیه اومده:
خیلی از مهندس‌ها تصور می‌کنن سازمانشون استراتژی مهندسی نداره! در حالی که در واقع، اغلبشون دارن؛ ولی ممکنه این حس ناشی از ناکارآمدی اسراتژی‌ها باشه. نویسنده، یعنی ویل لارسون (نویسنده کتاب‌هایی مثل Elegant Puzzle یا Staff Engineer) یک راهنمای کاربردی، و در عین حال غنی از مثال‌های واقعی، برای مسیریابی در پیچیدگی‌های فنی، و همچنین پیچیدگی‌های سازمانی از طریق «استراتژی ساختاریافته و هدفمند» ارائه می‌ده.

این کتاب که برای مهندس‌های ارشد، رهبران مهندسی، و معمارها نوشته شده. مثال‌های واقعی از شرکت‌هایی مثل استرایپ، اوبر، و کَلم استخراج ارائه میده. چارچوب پیشنهادی نویسنده، برای شکل‌دهی تصمیم‌گیری‌های حیاتی در مورد مهاجرت سیستم‌ها، منسوخ کردن APIها، سرمایه‌گذاری‌های پلتفرم و موارد مشابه کاربرد داره. کتاب، در طول مسیر، یاد قراره تا یاد بده برنامه‌ریزی فنی رو با ارتباطات، حاکمیت، و تفکر سیستمی تقویت کنید. چه در حال شکل‌دهی به مسیر تیمتون باشید و چه رهبری یک ابتکار در سطح شرکت رو به عهده داشته باشین، «طراحی استراتژی مهندسی» به شما کمک می‌کنه تصمیم‌های سنجیده‌ای بگیرید که پایدار باشن.

دلیل معرفی این کتاب اول موضوعش بود، دوم اینکه من چند کتاب از این نویسنده رو خوندم و سبک نوشتار و مسیر پرداختن به موضعش رو دوست دارم (این یک نظر شخصیه و شاید برای شما صدق نکنه)

نسخه کاغذی با قیمت 36.92€ عرضه خواهد شد.
در مورد نویسنده
Please open Telegram to view this post
VIEW IN TELEGRAM
🚀 Fast SQL Bulk Inserts With C# and EF Core

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

وارد کردن رکوردها یکی‌یکی مثل این است که خشک‌شدن رنگ را در حرکت آهسته تماشا کنید! روش‌های سنتی اصلاً جواب نمی‌دهند.
پس یاد گرفتن تکنیک‌های bulk insert سریع با #C و EF Core تبدیل به یک مهارت ضروری می‌شود.

در این مقاله، چندین روش برای انجام bulk insert در #C بررسی می‌کنیم:
• Dapper
• EF Core
• EF Core Bulk extensions
• SQL Bulk Copy
• Entity Framework Extensions

تمام مثال‌ها بر اساس یک کلاس User و جدول Users در SQL Server هستند:
public class User
{
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string PhoneNumber { get; set; }
}

این لیست کامل تمام روش‌های bulk insert نیست. چند گزینه دیگر بررسی نشده‌اند، مثل تولید دستی SQL یا استفاده از Table-Valued parameters.

📌EF Core Simple Approach

بیایید با یک مثال ساده با EF Core شروع کنیم. ما یک ApplicationDbContext می‌سازیم، یک شی User اضافه می‌کنیم و SaveChangesAsync را صدا می‌زنیم. این کار باعث می‌شود هر رکورد به‌صورت جداگانه در دیتابیس insert شود. یعنی برای هر رکورد یک round trip به دیتابیس نیاز داریم.
using var context = new ApplicationDbContext();

foreach (var user in GetUsers())
{
context.Users.Add(user);

await context.SaveChangesAsync();
}

و نتایج دقیقاً همان‌قدر ضعیف هستند که انتظار دارید:
EF Core - Add one and save برای 100 کاربر: 20 ms

EF Core - Add one and save برای 1,000 کاربر: 260 ms

EF Core - Add one and save برای 10,000 کاربر: 8,860 ms

نتایج 100,000 و 1,000,000 رکورد حذف شدند چون اجرای آنها بیش از حد طولانی شد.

این مثال را به‌عنوان «چگونه bulk insert انجام ندهیم» در نظر بگیرید.

🧩Dapper Simple Insert

Dapper
یک SQL-to-object mapper ساده برای NET. است. این ابزار اجازه می‌دهد مجموعه‌ای از اشیا را به‌راحتی در دیتابیس insert کنیم.

من از قابلیت Dapper برای unwrap کردن یک collection داخل یک SQL INSERT statement استفاده می‌کنم.
using var connection = new SqlConnection(connectionString);
connection.Open();

const string sql =
@"
INSERT INTO Users (Email, FirstName, LastName, PhoneNumber)
VALUES (@Email, @FirstName, @LastName, @PhoneNumber);
";

await connection.ExecuteAsync(sql, GetUsers());

نتایج بسیار بهتر از مثال ابتدایی هستند:
Dapper - Insert range برای 100 کاربر: 10 ms

Dapper - Insert range برای 1,000 کاربر: 113 ms

Dapper - Insert range برای 10,000 کاربر: 1,028 ms

Dapper - Insert range برای 100,000 کاربر: 10,916 ms

Dapper - Insert range برای 1,000,000 کاربر: 109,065 ms


🔸️EF Core Add and Save

اما EF Core هم هنوز کنار نکشیده. مثال اول عمداً خیلی ضعیف پیاده‌سازی شده بود. EF Core می‌تواند چندین SQL statement را با هم batch کند، پس از این قابلیت استفاده می‌کنیم.

اگر یک تغییر ساده بدهیم، می‌توانیم عملکرد به‌مراتب بهتری بگیریم. اول، همهٔ اشیا را به ApplicationDbContext اضافه می‌کنیم. سپس فقط یک‌بار SaveChangesAsync را صدا می‌زنیم.

این EF یک batched SQL statement می‌سازد—یعنی چندین INSERT را با هم گروه می‌کند—و همه را یکجا به دیتابیس می‌فرستد. این کار تعداد round trip ها را کم می‌کند و باعث بهبود سرعت می‌شود.
using var context = new ApplicationDbContext();

foreach (var user in GetUsers())
{
context.Users.Add(user);
}

await context.SaveChangesAsync();

نتایج benchmark این پیاده‌سازی:
EF Core - Add all and save برای 100 کاربر: 2 ms

EF Core - Add all and save برای 1,000 کاربر: 18 ms

EF Core - Add all and save برای 10,000 کاربر: 203 ms

EF Core - Add all and save برای 100,000 کاربر: 2,129 ms

EF Core - Add all and save برای 1,000,000 کاربر: 21,557 ms

به یاد داشته باشید: Dapper برای insert کردن 1,000,000 رکورد حدود 109 ثانیه زمان گرفت. با EF Core و batched queries، همان کار را در حدود 21 ثانیه انجام می‌دهیم.
EF Core AddRange و Save

این یک جایگزین برای مثال قبلی است. به جای فراخوانی Add برای همهٔ آبجکت‌ها، می‌توانیم AddRange را صدا بزنیم و یک کالکشن ارسال کنیم.

می‌خواستم این پیاده‌سازی را نشان دهم چون آن را به نمونهٔ قبلی ترجیح می‌دهم.
using var context = new ApplicationDbContext();

context.Users.AddRange(GetUsers());

await context.SaveChangesAsync();

نتایج بسیار مشابه نمونهٔ قبلی هستند: 📊
EF Core - Add range و Save، برای ۱۰۰ کاربر: ۲ ms
EF Core - Add range و Save، برای ۱,۰۰۰ کاربر: ۱۸ ms
EF Core - Add range و Save، برای ۱۰,۰۰۰ کاربر: ۲۰۴ ms
EF Core - Add range و Save، برای ۱۰۰,۰۰۰ کاربر: ۲,۱۱۱ ms
EF Core - Add range و Save، برای ۱,۰۰۰,۰۰۰ کاربر: ۲۱,۶۰۵ ms

EF Core Bulk Extensions ⚡️

یک کتابخانهٔ فوق‌العاده به نام EF Core Bulk Extensions وجود دارد که می‌توانیم برای افزایش عملکرد استفاده کنیم. شما با این کتابخانه می‌توانید کارهای بیشتری از جمله bulk insert انجام دهید، بنابراین ارزش بررسی دارد. این کتابخانه متن‌باز است و دارای لایسنس Community می‌باشد اگر شرایط استفادهٔ رایگان را داشته باشید. بخش لایسنس را برای جزئیات بررسی کنید.

برای استفادهٔ ما، متد BulkInsertAsync گزینهٔ عالی است. می‌توانیم کالکشن آبجکت‌ها را ارسال کنیم و یک SQL bulk insert انجام خواهد شد.
using var context = new ApplicationDbContext();

await context.BulkInsertAsync(GetUsers());

عملکرد آن نیز شگفت‌انگیز است: 🚀
EF Core - Bulk Extensions، برای ۱۰۰ کاربر: ۱.۹ ms
EF Core - Bulk Extensions، برای ۱,۰۰۰ کاربر: ۸ ms
EF Core - Bulk Extensions، برای ۱۰,۰۰۰ کاربر: ۷۶ ms
EF Core - Bulk Extensions، برای ۱۰۰,۰۰۰ کاربر: ۷۴۲ ms
EF Core - Bulk Extensions، برای ۱,۰۰۰,۰۰۰ کاربر: ۸,۳۳۳ ms

برای مقایسه، ما با EF Core batched queries برای درج ۱,۰۰۰,۰۰۰ رکورد حدود ۲۱ ثانیه نیاز داشتیم. می‌توانیم همان کار را با کتابخانهٔ Bulk Extensions تنها در ۸ ثانیه انجام دهیم. 😎

SQL Bulk Copy 🧩

اگر نتوانیم عملکرد دلخواه را از EF Core بگیریم، می‌توانیم از SqlBulkCopy استفاده کنیم. SQL Server عملیات bulk copy را به صورت native پشتیبانی می‌کند، پس از آن استفاده کنیم.

این پیاده‌سازی کمی پیچیده‌تر از مثال‌های EF Core است. باید یک نمونه SqlBulkCopy را کانفیگ کنیم و یک DataTable ایجاد کنیم که شامل آبجکت‌هایی باشد که می‌خواهیم درج کنیم.
using var bulkCopy = new SqlBulkCopy(ConnectionString);

bulkCopy.DestinationTableName = "dbo.Users";

bulkCopy.ColumnMappings.Add(nameof(User.Email), "Email");
bulkCopy.ColumnMappings.Add(nameof(User.FirstName), "FirstName");
bulkCopy.ColumnMappings.Add(nameof(User.LastName), "LastName");
bulkCopy.ColumnMappings.Add(nameof(User.PhoneNumber), "PhoneNumber");

await bulkCopy.WriteToServerAsync(GetUsersDataTable());

با این حال، عملکرد فوق‌العاده سریع است: 🔥
SQL Bulk Copy، برای ۱۰۰ کاربر: ۱.۷ ms
SQL Bulk Copy، برای ۱,۰۰۰ کاربر: ۷ ms
SQL Bulk Copy، برای ۱۰,۰۰۰ کاربر: ۶۸ ms
SQL Bulk Copy، برای ۱۰۰,۰۰۰ کاربر: ۶۴۶ ms
SQL Bulk Copy، برای ۱,۰۰۰,۰۰۰ کاربر: ۷,۳۳۹ ms

در ادامه نحوهٔ ایجاد DataTable و پر کردن آن با لیست آبجکت‌ها آمده است: 🏗
DataTable GetUsersDataTable()
{
var dataTable = new DataTable();

dataTable.Columns.Add(nameof(User.Email), typeof(string));
dataTable.Columns.Add(nameof(User.FirstName), typeof(string));
dataTable.Columns.Add(nameof(User.LastName), typeof(string));
dataTable.Columns.Add(nameof(User.PhoneNumber), typeof(string));

foreach (var user in GetUsers())
{
dataTable.Rows.Add(
user.Email, user.FirstName, user.LastName, user.PhoneNumber);
}

return dataTable;
}
Entity Framework Extensions 💎

آیا می‌توانیم بهتر از SqlBulkCopy عمل کنیم؟
شاید، حداقل نتایج بنچمارک من نشان می‌دهد که می‌توانیم.

یک کتابخانهٔ فوق‌العاده دیگر به نام Entity Framework Extensions وجود دارد. این فقط یک کتابخانهٔ bulk insert نیست - بنابراین توصیه می‌کنم حتماً بررسی شود. با این حال، امروز آن را برای bulk insert استفاده خواهیم کرد.

برای استفادهٔ ما، متد BulkInsertOptimizedAsync گزینهٔ عالی است. می‌توانیم کالکشن آبجکت‌ها را ارسال کنیم و یک SQL bulk insert انجام خواهد شد. همچنین برخی بهینه‌سازی‌ها زیر هود انجام می‌دهد تا عملکرد بهتر شود.
using var context = new ApplicationDbContext();

await context.BulkInsertOptimizedAsync(GetUsers());

عملکرد آن فوق‌العاده است: ⚡️🔥
EF Core - Entity Framework Extensions، برای ۱۰۰ کاربر: ۱.۸۶ ms
EF Core - Entity Framework Extensions، برای ۱,۰۰۰ کاربر: ۶.۹ ms
EF Core - Entity Framework Extensions، برای ۱۰,۰۰۰ کاربر: ۶۶ ms
EF Core - Entity Framework Extensions، برای ۱۰۰,۰۰۰ کاربر: ۶۳۶ ms
EF Core - Entity Framework Extensions، برای ۱,۰۰۰,۰۰۰ کاربر: ۷,۱۰۶ ms


جمع‌بندی 🎯

کار SqlBulkCopy برای حداکثر سرعت و سادگی بهترین است. 👑
با این حال، Entity Framework Extensions عملکرد فوق‌العاده‌ای ارائه می‌دهند و هم‌زمان سهولت استفاده‌ای که EF Core دارد را حفظ می‌کنند. 💎⚡️

بهترین گزینه به نیازهای خاص پروژهٔ شما بستگی دارد:
فقط عملکرد مهم است؟ SqlBulkCopy راه حل شماست. 🔥

به سرعت عالی و توسعهٔ آسان نیاز دارید؟ EF Core انتخاب هوشمندانه است. ⚡️

به دنبال تعادل بین عملکرد و سهولت استفاده هستید؟ Entity Framework Extensions ⚖️

تصمیم با شماست که بهترین گزینه برای پروژهٔ خودتان کدام است.

امیدوارم مفید بوده باشد. 🙌

🔖هشتگ‌ها:
#EFCore #Dapper #SqlBulkCopy #BulkExtensions #EntityFrameworkExtensions #Performance #CSharp #Database #DotNet #Programming #BulkInsert
Forwarded from tech-afternoon (Amin Mesbahi)
🧮 به نهضت NoEstimates# بپیوندیم؟ هنوز Story Point برای تخمین اندازه تسک‌ها لازمه؟


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

مرگِ یک آیین قدیمی یا تکامل طبیعی؟

سوال تکراری همیشگی بعد از اینکه یک تیم یا شرکت تخمین Story Point رو کنار می‌گذاره؛ اینه: «خب جایگزینش چیه؟»
ولی عملا سوال بهتر اینه که اصلاً نیاز واقعی‌مون به Story Point چی بوده؟ و چرا به وجود اومده؟
این بحث، بحثِ خیلی از تیم‌های نوپا و بالغ دنیا طی سال‌های اخیر بوده.
عملا Story Point در سال‌های ابتدایی پیدایش مفهوم agile در توسعه نرم‌افزار، ناجی خیلی از تیم‌ها بود. یعنی وقتی تیم‌ها از تخمین زمانی (مثل person-hour یا person-day) خسته شده بودن و می‌دیدن این اعداد عموما دروغ می‌گن؛ Story Point با نگاه «بی‌خیال ساعت شید، فقط بگید این کار نسبت به اون یکی چقدر بزرگ‌تره» به وجود اومد. و واقعاً هم کمک کرد. تیم‌ها بهتر پیش‌بینی می‌کردن، Velocity داشتن، ظرفیت اسپرینت مشخص بود.

اما این سال‌ها بعضی از تیم‌ها Story Point رو کنار گذاشتن، مهم‌ترین دلایلی که من دیدم، اینا بوده:

۱. طی گذر زمان؛ Story Point تبدیل به دروغ شد!
همون چیزی که قرار بود جلوی دروغ رو بگیره، خودش تبدیل به دروغ شد.
چرا؟ چون مدیر محصول یا PO می‌گفت: «این فیچر باید تو همین اسپرینت جا بشه، پس ۸ نزنید، ۵ بزنید!»
یا توسعه‌دهنده از ترس فشار بعدی، همیشه عدد بزرگ‌تر می‌زد.
نتیجه؟ Velocity غیرواقعی، تخمین غیرواقعی، بی‌اعتمادی کامل. من از حدودای ۲۰۱۲-۲۰۱۳ یادم میاد که نهضت NoEstimates# با این گفتمان راه افتاده که تخمین، هزینه داره و اغلب دقتش اون‌قدری نیست که ارزش هزینه‌ش رو داشته باشه!

۲. تیم‌ها دیگه نیازی به «واحد خیالی» ندارن
وقتی تیم کوچیکه، در اثر تجربه و هم‌نشینی گاهی دیگه نیازی نمی‌بینن بگن «این ۵ پوینته، اون ۸ پوینته».
فقط می‌گن: «این کار حدود ۲-۳ روزه، اون یکی یه هفته».
و این تخمین «تقریبی و رنج‌دار» گاهی دقیق‌تر از Story Point در میاد!
از طرفی، تیم‌های خوب به جای اندازه تسک، روی شکل دادن (Shaping) فیچرها تمرکز می‌کنند و تخمین ریز نمی‌زنن.

۳. کار رو باید انجام داد چه یه روز چه ده روز!
بعضی تیم‌ها ماهیت تسک‌هاشون «ضرورتِ محصوله»، یعنی قابل حذف یا جایگزینی نیست، محصول هم باید تولید شه. لذا برای مدیر و شرکت مهمه که زودتر برسه، ولی اگر چند وقت دیرتر هم بشه، راهی جز پذیرش نداره. اینجاست که تیم می‌گه خب so what؟ تخمین بزنم که چی بشه؟ من که اول و آخر باید این کار رو انجام بدم!

۴. کارهای روتین، یا قابل پیش‌بینی هستن
وقتی کارها به طور نسبی زمان مشابهی برای اجرا نیاز دارن، یا توی چند دسته‌ی قابل پیش‌بینی قرار می‌گیرن، تکرار مکررات باعث می‌شه تیم بیخیال تخمین بشه و می‌دونه چه کاری حدودا طی چی زمانی انجام می‌شه. اینجا دیگه به جای story point گاها T-Shirt sizing هم جواب می‌ده (S, M, L, XL)

پس کِی و کجا Story Point هنوز مفیده؟

- انترپرایزها: تیم بزرگ، سازمان بزرگ، وابستگی‌های بین تیمی زیاد، مدیریت بودجه سخت‌گیرانه و... توی چنین محیط‌هایی اینقدر در هم‌تنیدگی کارها، بوجه، تیم‌ها، و.. زیاده که یکی از مهارت‌های مدیر تیم، کمک به بهبود و تدقیق تخمین‌هاست. اینقدری که توی شرکت‌های بزرگ tech با ماشین‌لرنینگ و هوش‌مصنوعی این تخمین‌ها بهبود پیدا می‌کنه، چون دیر رسیدن یک ماهه‌ی یک محصول گاها عدم‌النفع یا حتی خسارت‌های چند ده میلیون دلاری می‌تونه به بار بیاره.
قبلا همینجا هم نوشتم که انترپرایز الزاما تعداد کارمند زیاد نیست، باید ساختار و رویه‌ها قابل انترپرایز خطاب شدن باشه!

- تیم‌های جدید که هنوز Flow پایداری ندارن

- وقتی تیم‌ها توزیع‌شده (distributed) هستن و ارتباط لحظه‌ای کمه

یادمون نره که Agile یعنی انعطاف. اگر ابزاری به درد تیم شما نمی‌خورد، کنارش بگذارید. اینکه process templateهای متنوعی وجود داره، از مدل‌های ساده مثل kanban تا scrum و agile و حتی مدل‌های خیلی سخت‌گیر و دقیقی مثل CMMI هم هست، یعنی «نسخه واحد برای همه تیم‌ها و محصولات وجود نداره».

سوال این نیست که "Story Point خوبه یا بد؟"
سوال اینه: "برای تیم ما، در مرحله فعلی، چه چیزی بهترین کمک رو می‌کنه؟"


بعضی‌ها هم که story point رو گذاشتن کنار از سر بلد نبودنشون بوده! نه از سر مناسب نبودنش! و اساسا باید عارضه رو در خودشون پیدا می‌کردن یا اینکه تا زمان بلوغ، می‌رفتن سراغ متد دیگه‌ای که مبتنی بر SP نباشه، نه اینکه متد رو نگه دارن و SP رو از دلش بکشن بیرون.

این موضوع خیلی مفصله ولی امیدوارم در حد سرنخ دادن کمک کرده باشه... 😊
Please open Telegram to view this post
VIEW IN TELEGRAM
I don't love programming for the same reasons as when I started.

What started like infinite Lego bricks has changed over time for me.

Don't get me wrong -- I still love building things. I still love being able to solve complex problems by putting systems together.

There's genuinely something awesome about seeing parts of a system come together.

But there's something that excites me more. More exciting than when I was building text-based RPGs when I was a kid.

It comes down to people.

As software engineers, we're going to have projects and challenges that are draining. They might seem like a mountain to climb or a project with no end in sight.

But if you're fortunate enough to work around other developers that:
• Don't shy away from crazy ideas
• Are driven to solve problems
• Aren't afraid of challenges
• Iterate quickly

I've found that this always gives me tons of energy. Far more than I've ever had from working on projects by myself -- regardless of how daunting the challenge at hand might be.

What gets your energy levels up on a project?
گزارش‌دهی PDF انعطاف‌پذیر در NET. با استفاده از Razor Views 📄

هرگز فراموش نمی‌کنم زمانی که روی پروژه‌ای کار می‌کردم که نیاز به تولید گزارش‌های فروش هفتگی برای یک مشتری داشت. راه‌حل اولیه شامل فرآیندی خسته‌کننده بود: خروجی گرفتن از داده‌ها، دستکاری آن‌ها در spreadsheets، و ساخت PDFها به صورت دستی. 😓 این روش وقت‌گیر، پرخطا و انرژی‌بر بود. من می‌دانستم که حتماً باید راه بهتری وجود داشته باشد. 💡

در این زمان بود که قدرت ترکیب Razor views با تبدیل HTML به PDF را کشف کردم. شما کنترل بیشتری روی فرمت‌دهی سند دارید. می‌توانید از CSS مدرن برای استایل‌دهی HTML استفاده کنید، که هنگام خروجی گرفتن به PDF اعمال خواهد شد. این کار همچنین در ASP.NET Core به راحتی قابل پیاده‌سازی است. 🖥

📌 در این مقاله، موارد زیر را پوشش می‌دهیم:

• درک Razor views
• تبدیل Razor views به HTML
• تبدیل HTML به PDF در .NET
• ترکیب همه چیز با Minimal APIs
بیایید شروع کنیم!

🚀Razor Views

یک قالب HTML با markupهای تعبیه‌شده Razor است. Razor به شما اجازه می‌دهد تا کد NET. را داخل یک صفحه وب بنویسید و اجرا کنید. فایل‌های Views دارای پسوند ویژه cshtml. هستند و معمولاً در ASP.NET Core MVC، Razor Pages و Blazor استفاده می‌شوند.
با این حال، می‌توانید Razor views را در یک class library یا پروژه ASP.NET Core Web API تعریف کنید.

می‌توانید از Model برای ارسال داده به Razor view استفاده کنید. داخل فایل .cshtml، نوع مدل را با کلمه کلیدی model@ مشخص می‌کنید. در مثال زیر، مشخص کرده‌ایم که کلاس Invoice مدل این view است. می‌توانید به نمونه مدل از طریق property Model در view دسترسی داشته باشید.

این همان view به نام InvoiceReport.cshtml است که برای تولید یک فاکتور PDF استفاده خواهیم کرد.

می‌توانید CSS را در Razor view به صورت inline بنویسید یا به یک stylesheet ارجاع دهید. من از فریم‌ورک Tailwind CSS استفاده می‌کنم که CSS به صورت inline است. معمولاً این کار را به یک front-end engineer در تیمم می‌سپارم تا گزارش را مطابق نیاز استایل‌دهی کند. 🎨
@using System.Globalization
@using HtmlToPdf.Contracts

@model HtmlToPdf.Contracts.Invoice

@{
    IFormatProvider cultureInfo = CultureInfo.CreateSpecificCulture("en-US");
    var subtotal = Model.LineItems.Sum(li => li.Price * li.Quantity).ToString("C", cultureInfo);
    var total = Model.LineItems.Sum(li => li.Price * li.Quantity).ToString("C", cultureInfo);
}

<noscript src="https://cdn.tailwindcss.com"></noscript>

<div class="min-w-7xl flex flex-col bg-gray-200 space-y-4 p-10">
    <h1 class="text-2xl font-semibold">Invoice #@Model.Number</h1>

    <p>Issued date: @Model.IssuedDate.ToString("dd/MM/yyyy")</p>
    <p>Due date: @Model.DueDate.ToString("dd/MM/yyyy")</p>

    <div class="flex justify-between space-x-4">
        <div class="bg-gray-100 rounded-lg flex flex-col space-y-1 p-4 w-1/2">
            <p class="font-medium">Seller:</p>
            <p>@Model.SellerAddress.CompanyName</p>
            <p>@Model.SellerAddress.Street</p>
            <p>@Model.SellerAddress.City</p>
            <p>@Model.SellerAddress.State</p>
            <p>@Model.SellerAddress.Email</p>
        </div>
        <div class="bg-gray-100 rounded-lg flex flex-col space-y-1 p-4 w-1/2">
            <p class="font-medium">Bill to:</p>
            <p>@Model.CustomerAddress.CompanyName</p>
            <p>@Model.CustomerAddress.Street</p>
            <p>@Model.CustomerAddress.City</p>
            <p>@Model.CustomerAddress.State</p>
            <p>@Model.CustomerAddress.Email</p>
        </div>
    </div>

    <div class="flex flex-col bg-white rounded-lg p-4 space-y-2">
        <h2 class="text-xl font-medium">Items:</h2>
        <div class="">
            <div class="flex space-x-4 font-medium">
                <p class="w-10">#</p>
                <p class="w-52">Name</p>
                <p class="w-20">Price</p>
                <p class="w-20">Quantity</p>
            </div>
@foreach ((int index, LineItem item) in Model.LineItems.Select((li, i) => (i + 1, li)))
{
<div class="flex space-x-4">
<p class="w-10">@index</p>
<p class="w-52">@item.Name</p>
<p class="w-20">@item.Price.ToString("C", cultureInfo)</p>
<p class="w-20">@item.Quantity.ToString("N2")</p>
</div>
}
</div>
</div>

<div class="flex flex-col items-end bg-gray-50 space-y-2 p-4 rounded-lg">
<p>Subtotal: @subtotal</p>
<p>Total: <span class="font-semibold">@total</span></p>
</div>
</div>
تبدیل Razor Views به HTML 🖥➡️📄

گام بعدی که نیاز داریم، راهی برای تبدیل Razor view به HTML است. می‌توانیم از کتابخانه Razor.Templating.Core استفاده کنیم. این کتابخانه یک API ساده ارائه می‌دهد تا فایل .cshtml را به یک رشته (string) تبدیل کند.
Install-Package Razor.Templating.Core

می‌توانید از کلاس static RazorTemplateEngine برای فراخوانی متد RenderAsync استفاده کنید. این متد مسیر Razor view و نمونه مدل (model) که به view منتقل می‌شود را می‌پذیرد.

نمونه کد:
Invoice invoice = invoiceFactory.Create();

string html = await RazorTemplateEngine.RenderAsync(
"Views/InvoiceReport.cshtml",
invoice);

همچنین می‌توانید به جای کلاس static از IRazorTemplateEngine استفاده کنید. در این حالت باید متد AddRazorTemplating را برای ثبت سرویس‌های مورد نیاز با DI فراخوانی کنید. این کار همچنین زمانی لازم است که بخواهید از dependency injection در داخل Razor views با @inject استفاده کنید. توصیه می‌شود بعد از ثبت تمام dependencies، AddRazorTemplating را فراخوانی کنید.
services.AddRazorTemplating();


تبدیل HTML به PDF 📄

حالا که Razor view ما به HTML تبدیل شد، می‌توانیم از آن برای تولید یک گزارش PDF استفاده کنیم. کتابخانه‌های زیادی این قابلیت را ارائه می‌دهند. کتابخانه‌ای که من بیشترین استفاده را از آن داشته‌ام IronPDF است. این یک کتابخانه پولی است (و واقعاً ارزشش را دارد)، اما می‌دانم توسعه‌دهندگان به گزینه‌های رایگان هم علاقه دارند، بنابراین چند گزینه رایگان را در انتها معرفی می‌کنم.

می‌توانیم از ChromePdfRenderer در IronPDF استفاده کنیم، که از مرورگر Chrome تعبیه‌شده استفاده می‌کند. این renderer متد RenderHtmlAsPdf را ارائه می‌دهد که یک PdfDocument تولید می‌کند. پس از داشتن سند، می‌توانید آن را در سیستم فایل ذخیره کنید یا به صورت داده باینری صادر کنید.
var renderer = new ChromePdfRenderer();

using var pdfDocument = renderer.RenderHtmlAsPdf(html);

pdfDocument.SaveAs($"invoice-{invoice.Number}.pdf");

اگر به دنبال گزینه‌های رایگان هستید، کتابخانه Puppeteer Sharp را بررسی کنید. این یک نسخه NET. از کتابخانه Puppeteer است و امکان اجرای headless Chrome را فراهم می‌کند.

گزینه دیگر که به صورت مشروط رایگان است، NReco.PdfGenerator می‌باشد، اما تنها برای استقرار روی یک سرور (single-server) رایگان است.

ترکیب همه چیز با هم 🛠

بیایید همه مواردی که بررسی کردیم را استفاده کنیم تا یک Minimal API endpoint برای تولید گزارش PDF فاکتور بسازیم و آن را به صورت فایل بازگردانیم.
app.MapGet("invoice-report", async (InvoiceFactory invoiceFactory) =>
{
Invoice invoice = invoiceFactory.Create();

var html = await RazorTemplateEngine.RenderAsync(
"Views/InvoiceReport.cshtml",
invoice);

var renderer = new ChromePdfRenderer();

using var pdfDocument = renderer.RenderHtmlAsPdf(html);

return Results.File(
pdfDocument.BinaryData,
"application/pdf",
$"invoice-{invoice.Number}.pdf");
});

این همان PDF گزارش تولید شده است که خروجی می‌دهد: 📄
خلاصه 📝

در این مقاله، ما قدرت استفاده از Razor views برای گزارش‌دهی PDF انعطاف‌پذیر در NET. را بررسی کردیم. ما دیدیم چگونه می‌توان قالب‌های گزارش را با Razor views ایجاد کرد، آن‌ها را به HTML تبدیل کرد و سپس آن HTML را به اسناد PDF با فرمت زیبا تبدیل نمود. 📄💻

فرقی نمی‌کند که بخواهید فاکتور، گزارش فروش یا هر نوع سند ساختاریافته دیگری تولید کنید، این روش یک راهکار ساده و قابل تنظیم ارائه می‌دهد. ⚡️

همچنان فوق‌العاده بمانید! 😎🚀

🔖هشتگ‌ها:
#DotNet #RazorViews #PDFReporting #QuestPDF #HTMLtoPDF
MVP Wall at Microsoft Ignite🔥
🧩 راحتیِ اشتباهِ “Happy Path”: جدا کردن سرویس‌ها از یکدیگر

بیایید صادق باشیم: همهٔ ما این کد را نوشته‌ایم. 😅
صبح دوشنبه است، یک ددلاین دارید، و باید یک قابلیت ثبت‌نام کاربر را پیاده‌سازی کنید. کار ساده‌ای است: کاربر را ذخیره کنید، یک ایمیل خوش‌آمد بفرستید، و ثبت‌نام را در داشبورد analytics خود ثبت کنید.
شما این را می‌نویسید:
public class UserService(
IUserRepository userRepository,
IEmailService emailService,
IAnalyticsService analyticsService)
{
public async Task RegisterUser(string email, string password)
{
var user = new User(email, password);
await userRepository.SaveAsync(user);

// 1. Directly coupled to email service (external API)
await emailService.SendWelcomeEmail(user.Email);

// 2. Directly coupled to analytics (this could be an external API)
await analyticsService.TrackUserRegistration(user.Id);

// What if we need to add more features?
// This method will keep growing...
}
}

ظاهرش تمیز است. خوانا است. روی ماشین شما هم کار می‌کند.
اما این متد یک بمب ساعتی است. 💣

این متد فرض می‌کند فقط Happy Path وجود دارد.
فرض می‌کند شبکه همیشه پایدار است، سرویس ایمیل همیشه در دسترس است، و API مربوط به analytics همیشه سریع است.
در محیط production هیچ‌کدام تضمین‌شده نیست. ⚠️

اگر بیشتر فکر کنید، احتمالاً مثال مشابهی را در پروژه‌های خودتان هم دیده‌اید. ممکن است همین سناریو نباشد، اما الگو یکسان است:
یک متد که به شکل خطی چندین Side Effect را مدیریت می‌کند.

بیایید بررسی کنیم چرا این کد خطرناک است و چطور می‌توان آن را به یک معماری Event-driven مقاوم تبدیل کرد. 🚀

🔍 خطرات پنهان در “God Method”

سه مشکل اساسی در همین ده خط کد وجود دارد.
1️⃣ Temporal Coupling (تأخیر و زمان‌وابستگی)

وقتی کاربر روی "Register" کلیک می‌کند، باید منتظر بماند برای:
Database
SMTP Server
Analytics API
اگر سرویس analytics امروز حالش خوب نباشد و پاسخ‌گویی‌اش ۳ ثانیه طول بکشد،
کاربر شما هم باید ۳ ثانیه صبر کند.

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

2️⃣ Partial Failure State (وضعیت شکست جزئی) 💥

این مهم‌ترین ریسک است. تصور کنید:

SaveAsync(user) موفق می‌شود →
کاربر در DB ذخیره می‌شود.

SendWelcomeEmail موفق می‌شود
→ کاربر ایمیل خوش‌آمد را دریافت می‌کند.

TrackUserRegistration یک خطای
503 Service Unavailable می‌دهد.

حالا چه؟ 🤔
اگر همه چیز را داخل transaction بگذارید و rollback کنید:
🔸️کاربر از دیتابیس حذف می‌شود
🔸️اما ایمیل خوش‌آمد را قبلاً دریافت کرده است
🔸️کاربر تلاش می‌کند وارد شود → وجود ندارد
🔸️یک تجربهٔ کاربری فاجعه‌بار.

اگر rollback نکنید:
🔹️کاربر در سیستم هست
🔹️اما در analytics ثبت نشده
🔹️عدم سازگاری داده (Data Inconsistency) ایجاد شده است
3️⃣ Violation of Single Responsibility (SRP)

❗️ شما ممکن است استدلال کنید که چون ما از interfaceها (مثل IEmailService) استفاده می‌کنیم، decoupled هستیم. این برای جزئیات پیاده‌سازی درست است، اما برای orchestration اشتباه است.
UserService در حال حاضر دو دلیل برای تغییر دارد:

• Core Domain Logic:
"اکنون علاوه بر email به username نیز نیاز داریم."

• Notification Policy:
"تیم Marketing می‌خواهد علاوه بر Email یک SMS هم ارسال شود."

UserService
باید فقط مسئول state change باشد (ایجاد کاربر).نباید مسئول orchestrating کردن side effectها باشد.
🔥 این نقض کامل اصل Single Responsibility است.

Level 1: Logical Decoupling with Domain Events

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

برای این کار از Domain Events استفاده می‌کنیم.
در اینجا نسخهٔ refactor شدهٔ UserService را می‌بینید:
public class UserService(
IUserRepository userRepository,
IDomainEventDispatcher dispatcher,
IUnitOfWork unitOfWork)
{
public async Task RegisterUser(string email, string password)
{
// 1. Create the User Entity
var user = new User(email, password);

// 2. Capture the side effect as an event object
var userRegisteredEvent = new UserRegisteredEvent(user.Id, user.Email);

// 3. Add the entity to the repository
await userRepository.AddAsync(user);

// 4. Dispatch the event (Assuming in-process dispatching here for simplicity)
// Note: Handlers for Email and Analytics are now completely separate classes.
await dispatcher.Dispatch(userRegisteredEvent);

await unitOfWork.SaveChangesAsync();
}
}

اکنون UserService پایدار است.
اگر فردا بخواهیم یک قابلیت جدید مثل "Loyalty Points" اضافه کنیم، این متد هیچ تغییری نمی‌کند.
فقط یک handler جدید برای UserRegisteredEvent اضافه می‌کنیم.

اما هنوز مشکل reliability حل نشده. اگر سیستم دقیقاً بعد از Dispatch ولی قبل از SaveChangesAsync کرش کند چه؟
ممکن است email ارسال شود اما user ذخیره نشود.
یا برعکس: user ذخیره شود اما event از دست برود.

Level 2: Reliability with the Outbox Pattern

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

باید تضمین کنیم که اگر User ذخیره شد، UserRegisteredEvent نیز ذخیره شود.

اینجاست که Outbox Pattern وارد می‌شود.
به جای ارسال مستقیم event به message bus، ما event را در یک جدول OutboxMessages ذخیره می‌کنیم.
و این کار در همان transaction ذخیرهٔ user انجام می‌شود.

اینجا پیاده‌سازی کامل logic را می‌بینید:
public async Task RegisterUser(string email, string password)
{
// 1. Create the Domain Event
var user = new User(email, password);
var domainEvent = new UserRegisteredEvent(user.Id, user.Email);

// 2. Open a Transaction
using var transaction = dbContext.Database.BeginTransaction();

try
{
// 3. Save the User to the Users Table
dbContext.Users.Add(user);

// 4. Serialize the Event and Save to Outbox Table
var outboxMessage = new OutboxMessage
{
Id = Guid.NewGuid(),
Type = nameof(UserRegisteredEvent),
Content = JsonSerializer.Serialize(domainEvent),
OccurredOn = DateTime.UtcNow,
ProcessedOn = null // Null means it hasn't been handled yet
};

dbContext.OutboxMessages.Add(outboxMessage);

// 5. Commit BOTH changes atomically
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}

اکنون یک background worker (در یک process جدا) جدول OutboxMessages را poll می‌کند.
پیام را برداشته و آن را به message bus (RabbitMQ، Azure Service Bus و...) publish می‌کند.

اگر email service از کار بیفتد؟worker دوباره تلاش می‌کند.با این روش به At-Least-Once Delivery رسیده‌ایم.
Level 3: Distributed Consistency with Sagas

🔄 الگویی برای ایجاد سازگاری توزیع‌شده با استفاده از Saga
الگوی Outbox برای side effectهایی مثل email که fire-and-forget هستند عالی است.
اما اگر عملیات بعدی اجباری باشد چه؟
📌 سناریو:
وقتی یک کاربر ثبت‌نام می‌کند، باید یک crypto-wallet برای او در WalletService ایجاد کنیم.
اگر ساخت wallet شکست بخورد (مثلاً به دلیل مسائل قانونی)، نباید اجازه دهیم کاربر در سیستم ما باقی بماند.
در چنین حالتی نمی‌توانیم بگوییم "بعداً retry کن".
اگر WalletService پاسخ دهد که "Fraud Detected"، باید ایجاد کاربر را undo کنیم.
این یک Distributed Transaction است، و با Saga Pattern مدیریت می‌شود.
یک Saga، دنباله‌ای از مراحل را هماهنگ می‌کند. اگر یکی از مراحل شکست بخورد، Compensating Transactions اجرا می‌شوند تا مراحل قبلی undo شوند.

Flow: Choreography-based Saga

💥 در ادامه نحوهٔ مدیریت سناریوی Failure در یک Saga مبتنی بر Choreography را می‌بینید:
گام‌به‌گام:
UserService: Creates User → Publishes UserCreated

WalletService: Listens to UserCreated → Tries to create wallet
•Failure: Wallet creation fails
•Action: Publishes WalletCreationFailed

UserService: Listens to WalletCreationFailed → Deletes/Deactivates the User

با این روش به Eventual Consistency می‌رسیم.
ممکن است سیستم برای چند ثانیه ناسازگار باشد (کاربر وجود دارد ولی wallet ندارد)، اما در نهایت به یک وضعیت معتبر می‌رسد (کاربر حذف می‌شود).

Summary: A Heuristic for Decision Making

🧠 شما لازم نیست برای همه‌چیز از Saga استفاده کنید.Over-engineering به همان اندازه بد است که Tight Coupling.
از این قاعدهٔ ساده استفاده کنید:
1️⃣ آیا این یک notification ساده است؟
(Email، Analytics، Cache Invalidation)
→ از Domain Events + Outbox استفاده کنید.
اشکالی ندارد ۵ ثانیه بعد پردازش شود.

2️⃣ آیا این یک وابستگی حیاتی در Business rule است؟
(Payments، Inventory، Account Status)
→ از Saga استفاده کنید.
اگر مرحلهٔ B شکست بخورد، مرحلهٔ A باید revert شود.

کاپلینگ فقط دربارهٔ ساختار کد نیست.
دربارهٔ درک مرزهای failure است.
اگر Analytics Service از کار بیفتد، نباید مانع ثبت‌نام کاربر شود.
سیستم‌ها را برای survive کردن در برابر Unhappy Path بسازید.
امیدوارم مفید بوده باشد.
🔗Link
🔖هشتگ‌ها:
#softwarearchitecture #distributed_systems #saga_pattern #eventdriven #ddd #cleanarchitecture #outbox_pattern
Options Pattern in ASP.NET Core🧩
الگوی Options در ASP.NET Core 🎛

الگوی Options pattern از کلاس‌ها برای ارائهٔ دسترسی strongly typed به گروهی از تنظیمات مرتبط استفاده می‌کند.
وقتی تنظیمات پیکربندی (Configuration Settings) بر اساس سناریو در کلاس‌های جداگانه ایزوله می‌شوند، برنامه به دو اصل مهم مهندسی نرم‌افزار پایبند می‌ماند:

1. Encapsulation 🔐

کلاس‌هایی که به تنظیمات پیکربندی وابسته هستند، فقط به همان تنظیماتی وابسته‌اند که واقعاً استفاده می‌کنند.

2. Separation of Concerns 🧩

تنظیمات بخش‌های مختلف برنامه به یکدیگر وابسته یا Coupled نیستند.
ءOptions همچنین یک مکانیزم برای اعتبارسنجی داده‌های پیکربندی فراهم می‌کند.

ءBind کردن پیکربندی سلسله‌مراتبی 🔗

روش پیشنهادی برای خواندن مقادیر پیکربندی مرتبط، استفاده از Options Pattern است.

برای مثال، فرض کنید مقادیر پیکربندی زیر را داریم:
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}

ایجاد کلاس PositionOptions 📦
public class PositionOptions
{
public const string Position = "Position";

public string Title { get; set; } = String.Empty;
public string Name { get; set; } = String.Empty;
}

ویژگی‌های یک Options class✨️
یک کلاس Options باید:

• غیر abstract باشد.
• شامل propertyهای public read-write برای مقادیری باشد که در config وجود دارند.
• ءpropertyهای read-write آن مطابق با ورودی‌های configuration مقداردهی شوند.

• فیلدها (Fields) مقداردهی نمی‌شوند.
در مثال بالا، فیلد Position مقداردهی نمی‌شود و تنها برای جلوگیری از hard-code کردن رشتهٔ "Position" استفاده می‌شود.

ءBind کردن تنظیمات و نمایش مقدار 📥📤

کد زیر:
متد ConfigurationBinder.Bind را فراخوانی می‌کند تا کلاس PositionOptions را به سکشن Position bind کند.داده‌های پیکربندی Position را نمایش می‌دهد.
public class Test22Model : PageModel
{
private readonly IConfiguration Configuration;

public Test22Model(IConfiguration configuration)
{
Configuration = configuration;
}

public ContentResult OnGet()
{
var positionOptions = new PositionOptions();
Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

return Content($"Title: {positionOptions.Title} \n" +
$"Name: {positionOptions.Name}");
}
}

در کد فوق، به‌صورت پیش‌فرض، تغییرات فایل JSON configuration بعد از اجرای برنامه نیز خوانده می‌شوند. 🔄

استفاده از <ConfigurationBinder.Get<T✨️

متد <ConfigurationBinder.Get<T نوع مشخص‌شده را Bind کرده و همان نوع را برمی‌گرداند.
در بسیاری از موارد، استفاده از <ConfigurationBinder.Get<T راحت‌تر از ConfigurationBinder.Bind است.
کد زیر نحوهٔ استفاده از <Get<T با کلاس PositionOptions را نشان می‌دهد:
public class Test21Model : PageModel
{
private readonly IConfiguration Configuration;
public PositionOptions? positionOptions { get; private set; }

public Test21Model(IConfiguration configuration)
{
Configuration = configuration;
}

public ContentResult OnGet()
{
positionOptions = Configuration.GetSection(PositionOptions.Position)
.Get<PositionOptions>();

return Content($"Title: {positionOptions.Title} \n" +
$"Name: {positionOptions.Name}");
}
}

در کد بالا، به‌صورت پیش‌فرض، تغییرات فایل JSON configuration بعد از اجرای برنامه نیز خوانده می‌شوند. 🔄
ءBind و مقداردهی برای کلاس abstract🔧

ءBind اجازه می‌دهد که یک کلاس abstract مقداردهی شود.

کد زیر از کلاس abstract زیر استفاده می‌کند:
namespace ConfigSample.Options;

public abstract class SomethingWithAName
{
public abstract string? Name { get; set; }
}

public class NameTitleOptions(int age) : SomethingWithAName
{
public const string NameTitle = "NameTitle";

public override string? Name { get; set; }
public string Title { get; set; } = string.Empty;

public int Age { get; set; } = age;
}

کد زیر مقداردهی NameTitleOptions را نمایش می‌دهد:
public class Test33Model : PageModel
{
private readonly IConfiguration Configuration;

public Test33Model(IConfiguration configuration)
{
Configuration = configuration;
}

public ContentResult OnGet()
{
var nameTitleOptions = new NameTitleOptions(22);
Configuration.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);

return Content($"Title: {nameTitleOptions.Title} \n" +
$"Name: {nameTitleOptions.Name} \n" +
$"Age: {nameTitleOptions.Age}"
);
}
}


تفاوت Bind و Get ⚖️

Bind:
• امکان مقداردهی یک کلاس abstract را می‌دهد.
• نیازی به ساخت instance ندارد.

Get<>:
• باید خودش یک instance بسازد.
• بنابراین فقط روی انواع concrete کار می‌کند.

Options Pattern🔖

روش دیگر هنگام استفاده از Options Pattern این است که سکشن Position را Bind کرده و آن را به Service Container اضافه کنیم.

کد زیر، کلاس PositionOptions را با Configure به DI اضافه می‌کند:
using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

اکنون می‌توانیم از طریق DI تنظیمات را بخوانیم:
public class Test2Model : PageModel
{
private readonly PositionOptions _options;

public Test2Model(IOptions<PositionOptions> options)
{
_options = options.Value;
}

public ContentResult OnGet()
{
return Content($"Title: {_options.Title} \n" +
$"Name: {_options.Name}");
}
}

در این روش، تغییرات فایل JSON بعد از شروع برنامه خوانده نمی‌شوند.
برای پشتیبانی از تغییرات runtime باید از IOptionsSnapshot استفاده کنید. 🔄

Options Interfaces 🔍
1️⃣ IOptions<TOptions>

پشتیبانی نمی‌کند از:
• خواندن تغییرات configuration بعد از start

• ءNamed options

• ءSingleton است → در تمام طول برنامه ثابت است.

• می‌تواند در هر service lifetime inject شود.

2️⃣ IOptionsSnapshot<TOptions>

• در سناریوهایی مفید است که تنظیمات باید در هر درخواست مجدداً محاسبه شوند.

• ءScoped است → نمی‌توان آن را در یک Singleton inject کرد.

• از Named options پشتیبانی می‌کند.

• برای خواندن تغییرات Runtime مناسب است. 🔁

3️⃣ IOptionsMonitor<TOptions>

• برای دریافت options و مدیریت Change Notification استفاده می‌شود.

• ءSingleton است.

پشتیبانی می‌کند از:
• Reloadable configuration

• Named options

• Selective invalidation (با IOptionsMonitorCache)

4️⃣ IOptionsFactory<TOptions>

مسئول ایجاد instanceهای جدید گزینه‌هاست.
پیکربندی‌ها را (IConfigureOptions و IPostConfigureOptions) اجرا می‌کند.

5️⃣ IOptionsMonitorCache<TOptions>

• کش داخلی IOptionsMonitor

• می‌تواند یک گزینه را حذف کند تا مقدار جدید دوباره محاسبه شود.

• امکان اضافه‌کردن دستی مقدار جدید با TryAdd

• متد Clear برای ریست تمام named options
استفاده از IOptionsSnapshot برای خواندن داده‌های به‌روزشده ⚙️📄


IOptionsSnapshot<TOptions>🧪:
🔹️تنظیمات (Options) در هر درخواست یک بار محاسبه می‌شوند و برای مدت زمان همان درخواست کش می‌شوند.

🔹️چون یک سرویس Scoped است و در هر درخواست دوباره محاسبه می‌شود، ممکن است باعث هزینهٔ کارایی شود.

🔹️زمانی تغییرات پیکربندی را پس از شروع برنامه می‌خواند که Provider مربوطه از بارگذاری مجدد پشتیبانی کند.

تفاوت IOptionsMonitor با IOptionsSnapshot🖇:


🔸️ءIOptionsMonitor یک Singleton است و همیشه مقدار لحظه‌ای تنظیمات را ارائه می‌دهد؛ مناسب برای سرویس‌های Singleton.

🔸️ءIOptionsSnapshot یک Scoped است و هنگام ایجاد شدن، یک Snapshot از تنظیمات می‌گیرد؛ مناسب برای سرویس‌های Transient و Scoped.

نمونهٔ استفاده از <IOptionsSnapshot<TOptions🧪
public class TestSnapModel : PageModel
{
private readonly MyOptions _snapshotOptions;

public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
{
_snapshotOptions = snapshotOptionsAccessor.Value;
}

public ContentResult OnGet()
{
return Content($"Option1: {_snapshotOptions.Option1} \n" +
$"Option2: {_snapshotOptions.Option2}");
}
}

ثبت MyOptions در DI 🧩
using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

در این حالت، تغییرات فایل JSON پس از شروع برنامه خوانده می‌شوند.

IOptionsMonitor 🛰

ثبت MyOptions در سرویس‌ها (مانند قبل)
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));


نمونهٔ استفاده از <IOptionsMonitor<TOptions 📡
public class TestMonitorModel : PageModel
{
private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
{
_optionsDelegate = optionsDelegate;
}

public ContentResult OnGet()
{
return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
$"Option2: {_optionsDelegate.CurrentValue.Option2}");
}
}

در این حالت نیز، تغییرات JSON بعد از شروع برنامه خوانده می‌شوند.

استفاده از ConfigurationKeyName برای تعیین کلید سفارشی 🔑

به‌طور پیش‌فرض، نام پراپرتی کلاس Options برابر با نام کلید در پیکربندی است.
اگر بین نام پراپرتی و نام کلید تفاوت وجود دارد، می‌توان از ConfigurationKeyName استفاده کرد.

مواقع استفاده:
• زمانی که نام کلید در پیکربندی یک شناسهٔ معتبر #C نیست.
• یا زمانی که می‌خواهید نام متفاوتی در کد داشته باشید.
مثال 🎯
public class PositionOptionsWithConfigurationKeyName
{
public const string Position = "Position";

[ConfigurationKeyName("position-noscript")]
public string Title { get; set; } = string.Empty;

[ConfigurationKeyName("position-name")]
public string Name { get; set; } = string.Empty;
}

فایل appsettings.json
{
"Position": {
"position-noscript": "Editor",
"position-name": "Joe Smith"
}
}

در نتیجه،
Title ← مقدار position-noscript
Name ← مقدار position-name

پشتیبانی از Named Options با استفاده از IConfigureNamedOptions 🎯⚙️

🔹️ءNamed Options چه هستند؟
ءNamed Options زمانی مفید هستند که:

• چند بخش متفاوت از configuration نیاز دارند به یک کلاس مشترک Bind شوند.

• نام‌گذاری‌ها Case-Sensitive هستند.

• بتوانیم چند نسخهٔ متفاوت از یک Options را با Names مختلف مدیریت کنیم.

مثال فایل appsettings.json 📄
{
"TopItem": {
"Month": {
"Name": "Green Widget",
"Model": "GW46"
},
"Year": {
"Name": "Orange Gadget",
"Model": "OG35"
}
}
}

در این مثال، دو بخش داریم:
TopItem:Month
TopItem:Year
به‌جای تعریف دو کلاس جداگانه، از یک کلاس مشترک استفاده می‌کنیم:
کلاس مشترک TopItemSettings 🧱
public class TopItemSettings
{
public const string Month = "Month";
public const string Year = "Year";

public string Name { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
}