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)
بعد از دیدن مطلبی که مسعود بیگی در کانال تندتک درباره حذف 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?
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
🧩 راحتیِ اشتباهِ “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 در 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;
}
پیکربندی Named Options در DI 🛠
اینجا دو Named Options میسازیم:
TopItemSettings.Month
TopItemSettings.Year
استفاده از Named Options با IOptionsSnapshot 🔍📦
در کد بالا:
• ءSnapshot مربوط به Month گرفته میشود.
• ءSnapshot مربوط به Year گرفته میشود.
🔸️تمام Options در واقع Named Instance هستند.
🔹️ء<IConfigureOptions<T> برای Default Name اعمال میشود، یعنی نام خالی ("").
🔸️ء<IConfigureNamedOptions<T علاوهبر آنکه IConfigureOptions را هم پیادهسازی میکند، برای Named Options مخصوص استفاده میشود.
🔹️در <IOptionsFactory<TOptions منطق انتخاب نام وجود دارد:
🔸️اگر نام Null باشد یعنی Configure روی همهٔ Named Options اعمال میشود.
🔹️متدهای ConfigureAll و PostConfigureAll از همین رفتار استفاده میکنند.
این باعث میشود بتوانید:
• یک پیکربندی خاص را برای یک نام خاص تنظیم کنید.
• و یک پیکربندی کلی را برای همه نامها اعمال کنید.
چرا OptionsBuilder؟
ء<OptionsBuilder<TOptions فرایند ساخت Named Options را سادهتر میکند چون:
تنها در یک جای اولیه نام option مشخص میشود:
اما در بقیهٔ متدهای Configure نیازی نیست نام را تکرار کنید.
ءValidation Options فقط با OptionsBuilder قابل انجام است.
متدهایی که Service Dependency دریافت میکنند نیز فقط با OptionsBuilder پشتیبانی میشوند.
نمونهٔ استفاده
در بخش Options Validation به کار میرود.
سرویسها را میتوان هنگام پیکربندی options به دو روش از dependency injection دریافت کرد:
ارسال یک configuration delegate به متد Configure روی <OptionsBuilder<TOptions.
ء<OptionsBuilder<TOptions چندین overload از Configure ارائه میدهد که اجازه میدهد تا حداکثر پنج سرویس برای پیکربندی options استفاده شوند:
ایجاد یک نوع (class) که <IConfigureOptions<TOptions یا <IConfigureNamedOptions<TOptions را پیادهسازی کند و ثبت آن بهعنوان سرویس.
توصیه میشود از روش ارسال configuration delegate به Configure استفاده کنید، زیرا ایجاد یک سرویس پیچیدهتر است. ایجاد نوع دقیقاً معادل کاری است که فریمورک هنگام فراخوانی Configure انجام میدهد.
فراخوانی Configure یک سرویس transient از نوع generic IConfigureNamedOptions<TOptions ثبت میکند که دارای سازندهای است که سرویسهای generic مشخصشده را میپذیرد.
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
اینجا دو Named Options میسازیم:
TopItemSettings.Month
TopItemSettings.Year
استفاده از Named Options با IOptionsSnapshot 🔍📦
public class TestNOModel : PageModel
{
private readonly TopItemSettings _monthTopItem;
private readonly TopItemSettings _yearTopItem;
public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
{
_monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
_yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
}
public ContentResult OnGet()
{
return Content($"Month:Name {_monthTopItem.Name} \n" +
$"Month:Model {_monthTopItem.Model} \n\n" +
$"Year:Name {_yearTopItem.Name} \n" +
$"Year:Model {_yearTopItem.Model} \n");
}
}
در کد بالا:
• ءSnapshot مربوط به Month گرفته میشود.
• ءSnapshot مربوط به Year گرفته میشود.
توضیح ساختار Options و NamedOptions 🧩
🔸️تمام Options در واقع Named Instance هستند.
🔹️ء<IConfigureOptions<T> برای Default Name اعمال میشود، یعنی نام خالی ("").
🔸️ء<IConfigureNamedOptions<T علاوهبر آنکه IConfigureOptions را هم پیادهسازی میکند، برای Named Options مخصوص استفاده میشود.
🔹️در <IOptionsFactory<TOptions منطق انتخاب نام وجود دارد:
🔸️اگر نام Null باشد یعنی Configure روی همهٔ Named Options اعمال میشود.
🔹️متدهای ConfigureAll و PostConfigureAll از همین رفتار استفاده میکنند.
این باعث میشود بتوانید:
• یک پیکربندی خاص را برای یک نام خاص تنظیم کنید.
• و یک پیکربندی کلی را برای همه نامها اعمال کنید.
OptionsBuilder API 🏗✨
چرا OptionsBuilder؟
ء<OptionsBuilder<TOptions فرایند ساخت Named Options را سادهتر میکند چون:
تنها در یک جای اولیه نام option مشخص میشود:
services.AddOptions<TOptions>("MyName")اما در بقیهٔ متدهای Configure نیازی نیست نام را تکرار کنید.
ءValidation Options فقط با OptionsBuilder قابل انجام است.
متدهایی که Service Dependency دریافت میکنند نیز فقط با OptionsBuilder پشتیبانی میشوند.
نمونهٔ استفاده
در بخش Options Validation به کار میرود.
استفاده از سرویسهای DI برای پیکربندی Options ⚙️📦
سرویسها را میتوان هنگام پیکربندی options به دو روش از dependency injection دریافت کرد:
ارسال یک configuration delegate به متد Configure روی <OptionsBuilder<TOptions.
ء<OptionsBuilder<TOptions چندین overload از Configure ارائه میدهد که اجازه میدهد تا حداکثر پنج سرویس برای پیکربندی options استفاده شوند:
builder.Services.AddOptions<MyOptions>("optionalName")
.Configure<Service1, Service2, Service3, Service4, Service5>(
(o, s, s2, s3, s4, s5) =>
o.Property = DoSomethingWith(s, s2, s3, s4, s5));ایجاد یک نوع (class) که <IConfigureOptions<TOptions یا <IConfigureNamedOptions<TOptions را پیادهسازی کند و ثبت آن بهعنوان سرویس.
توصیه میشود از روش ارسال configuration delegate به Configure استفاده کنید، زیرا ایجاد یک سرویس پیچیدهتر است. ایجاد نوع دقیقاً معادل کاری است که فریمورک هنگام فراخوانی Configure انجام میدهد.
فراخوانی Configure یک سرویس transient از نوع generic IConfigureNamedOptions<TOptions ثبت میکند که دارای سازندهای است که سرویسهای generic مشخصشده را میپذیرد.
اعتبارسنجی Options ✅🔍
ءOptions validation امکان اعتبارسنجی مقدارهای options را فراهم میکند.
فایل appsettings.json زیر را در نظر بگیرید:
{
"MyConfig": {
"Key1": "My Key One",
"Key2": 10,
"Key3": 32
}
}کلاس زیر برای Bind شدن به بخش "MyConfig" استفاده میشود و چند قانون DataAnnotations را اعمال میکند:
public class MyConfigOptions
{
public const string MyConfig = "MyConfig";
[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public string Key1 { get; set; }
[Range(0, 1000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int Key2 { get; set; }
public int Key3 { get; set; }
}
کد زیر:
ءAddOptions را فراخوانی میکند تا <OptionsBuilder<MyConfigOptions را دریافت کند که به کلاس MyConfigOptions Bind میشود.
ءValidateDataAnnotations را فراخوانی میکند تا اعتبارسنجی با استفاده از DataAnnotations فعال شود.
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations();
متد ValidateDataAnnotations در پکیج Microsoft.Extensions.Options.DataAnnotations قرار دارد. برای web appهایی که از SDK نوع Microsoft.NET.Sdk.Web استفاده میکنند، این پکیج بهطور ضمنی از shared framework مرجع میشود.
کد زیر مقدارهای configuration یا خطاهای validation را نمایش میدهد:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IOptions<MyConfigOptions> _config;
public HomeController(IOptions<MyConfigOptions> config,
ILogger<HomeController> logger)
{
_config = config;
_logger = logger;
try
{
var configValue = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
_logger.LogError(failure);
}
}
}
public ContentResult Index()
{
string msg;
try
{
msg = $"Key1: {_config.Value.Key1} \n" +
$"Key2: {_config.Value.Key2} \n" +
$"Key3: {_config.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
return Content(optValEx.Message);
}
return Content(msg);
}
}
کد زیر یک قانون پیچیدهتر اعتبارسنجی را با استفاده از یک delegate اعمال میکند:
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Key2 != 0)
{
return config.Key3 > config.Key2;
}
return true;
}, "Key3 must be > than Key2."); // Failure message.
IValidateOptions<TOptions> و IValidatableObject ✨
کلاس زیر رابط <IValidateOptions<TOptions را پیادهسازی میکند:
public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
public MyConfigOptions _config { get; private set; }
public MyConfigValidation(IConfiguration config)
{
_config = config.GetSection(MyConfigOptions.MyConfig)
.Get<MyConfigOptions>();
}
public ValidateOptionsResult Validate(string name, MyConfigOptions options)
{
string? vor = null;
var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
var match = rx.Match(options.Key1!);
if (string.IsNullOrEmpty(match.Value))
{
vor = $"{options.Key1} doesn't match RegEx \n";
}
if ( options.Key2 < 0 || options.Key2 > 1000)
{
vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
}
if (_config.Key2 != default)
{
if(_config.Key3 <= _config.Key2)
{
vor += "Key3 must be > than Key2.";
}
}
if (vor != null)
{
return ValidateOptionsResult.Fail(vor);
}
return ValidateOptionsResult.Success;
}
}
ءIValidateOptions این امکان را فراهم میکند که کد اعتبارسنجی از فایل Program.cs خارج شده و در یک کلاس قرار گیرد. 🧩
با استفاده از کد بالا، اعتبارسنجی در فایل Program.cs با کد زیر فعال میشود:
using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
MyConfigOptions.MyConfig));
builder.Services.AddSingleton<IValidateOptions
<MyConfigOptions>, MyConfigValidation>();
var app = builder.Build();
اعتبارسنجی Options همچنین از IValidatableObject پشتیبانی میکند. برای انجام اعتبارسنجی سطح کلاس در داخل خود کلاس:
رابط IValidatableObject و متد Validate را در کلاس پیادهسازی کنید.
در Program.cs متد ValidateDataAnnotations را فراخوانی کنید. ✔️
ValidateOnStart 🚀
اعتبارسنجی Options اولین باری که یک نمونه از TOption ساخته میشود اجرا میگردد. این یعنی مثلاً زمانیکه اولین دسترسی به IOptionsSnapshot<TOptions>.Value در pipeline یک درخواست رخ میدهد یا زمانیکه IOptionsMonitor<TOptions>.Get(string) فراخوانی میشود. پس از reload شدن تنظیمات، اعتبارسنجی دوباره اجرا میشود.
زمان اجرا از OptionsCache<TOptions> برای کش کردن نمونه ساختهشده استفاده میکند.
برای اجرای اعتبارسنجی هنگام شروع اپلیکیشن (Eager Validation) باید در Program.cs متد زیر را فراخوانی کنید:
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.ValidateOnStart();
Options post-configuration 🛠
ءPost-configuration را میتوان با <IPostConfigureOptions<TOptions انجام داد. Post-configuration پس از اجرای تمام <IConfigureOptions<TOptions انجام میشود:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});