🧑💻 محدودسازی نرخ کاربران بر اساس هویت (Identity)
اگر از کاربران میخواهید که در API شما احراز هویت (Authenticate) کنند، میتوانید تشخیص دهید که کاربر فعلی کیست. سپس میتوانید از هویت (Identity) کاربر به عنوان کلید پارتیشن (Partition Key) برای یک RateLimitPartition استفاده کنید. 🆔
💻 پیادهسازی محدودسازی با هویت کاربر
در اینجا نحوه ایجاد چنین سیاست محدودسازی نرخ آمده است:
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("fixed-by-user", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name?.ToString(),
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromMinutes(1)
}));
});
من از مقدار User.Identity در HttpContext استفاده میکنم تا Claim مربوط به Name کاربر فعلی را به دست آورم. این معمولاً متناظر با Claim با نام sub درون یک JWT است - که همان شناسه کاربر میباشد.
🛡 اعمال Rate Limiting روی Reverse Proxy
در یک پیادهسازی قوی، شما میخواهید محدودسازی نرخ را در سطح Reverse Proxy اعمال کنید، پیش از آنکه درخواست به API شما برسد. و اگر یک سیستم توزیع شده دارید، این یک الزام است. در غیر این صورت، سیستم شما به درستی کار نخواهد کرد. 🚨
پیادهسازیهای متعددی برای Reverse Proxy وجود دارد که میتوانید از بین آنها انتخاب کنید.
YARP
یک Reverse Proxy با یکپارچگی عالی با NET. است. این امر تعجبآور نیست، زیرا YARP با #C نوشته شده است.
⚙️ اعمال Rate Limiting در تنظیمات YARP
برای پیادهسازی محدودسازی نرخ روی Reverse Proxy با استفاده از YARP، شما باید:
یک سیاست محدودسازی نرخ تعریف کنید (همانند مثالهای قبلی).
RateLimiterPolicy
را برای مسیر (Route) در تنظیمات YARP پیکربندی کنید:
"products-route": {
"ClusterId": "products-cluster",
"RateLimiterPolicy": "sixty-per-minute-fixed",
"Match": {
"Path": "/products/{**catch-all}"
},
"Transforms": [
{ "PathPattern": "{**catch-all}" }
]
}توجه به حافظه: Middleware محدودسازی نرخ داخلی، از یک حافظه in-memory برای ردیابی تعداد درخواستها استفاده میکند. اگر میخواهید Reverse Proxy خود را در یک راهاندازی با در دسترس بودن بالا (High-Availability) اجرا کنید، به استفاده از یک Distributed Cache نیاز خواهید داشت. استفاده از یک Redis backplane برای محدودسازی نرخ، یک گزینه خوب برای بررسی است. 💾
📝 سخن پایانی
با استفاده از PartitionedRateLimiter میتوانید به راحتی سیاستهای محدودسازی نرخ جزئی (Granular) ایجاد کنید.
دو رویکرد رایج عبارتند از:
محدودسازی نرخ بر اساس آدرس IP 🌐
محدودسازی نرخ بر اساس شناسه کاربر (User Identifier) 👤
تیم NET. محدودسازی نرخ را ارائه کرد که بسیار شگفت انگیز است. اما پیادهسازی فعلی کاستیهایی دارد. مشکل اصلی این است که فقط به صورت in-memory کار میکند. برای یک راهکار توزیع شده (distributed)، شما باید خودتان چیزی پیادهسازی کنید یا از یک کتابخانه خارجی استفاده نمایید.
شما میتوانید از Reverse Proxy YARP برای ساختن سیستمهای توزیع شده قوی و مقیاسپذیر استفاده کنید. و اضافه کردن Rate Limiting در سطح Reverse Proxy تنها به چند خط کد نیاز دارد. بهتر است که به طور گستردهای از آن در سیستمهایمان استفاده میکنیم.
از اینکه این مقاله را خواندید، متشکرم. و فوقالعاده بمانید! ✨
🔖 هشتگها:
#DotNet #RateLimiting #Security #ASPNETCore #ReverseProxy #IPAddress
💡 شکست واقعی در Backend Engineering چیست؟
شکست در Backend Engineering، نوشتن کد بد نیست. ❌
خراب شدن یک دیپلوی (Deployment) نیست. 💥
طراحی یک شمای پایگاه داده (Schema) که مقیاسپذیر نیست، نیست. 📈
اینها فقط یادگیری هستند. 🧠
🛑 پس شکست واقعی چیست؟
شکست واقعی؟
این است که بخواهی یک توسعهدهنده (Dev) باشی، اما هرگز شروع نکنی. 🚪
بیشتر مردم منتظر میمانند تا:
• آموزش "کامل" را پیدا کنند.
• دوره "درست" را پیدا کنند.
• وقت آزاد بیشتری داشته باشند.
اما Backend Engineering تنها با انجام دادن، آموخته میشود: 💻
• نوشتن اولین API خود.
• شکستن اولین شمای پایگاه داده خود.
• دیپلوی کردن چیزی که به سختی کار میکند.
این اشتباهات بخشی از فرآیند هستند. 🧩
اجتناب از آنها همان چیزی است که شما را عقب نگه میدارد. 🛑
آشفته شروع کن. کوچک شروع کن. فقط شروع کن. ✨
شکست در Backend Engineering، نوشتن کد بد نیست. ❌
خراب شدن یک دیپلوی (Deployment) نیست. 💥
طراحی یک شمای پایگاه داده (Schema) که مقیاسپذیر نیست، نیست. 📈
اینها فقط یادگیری هستند. 🧠
🛑 پس شکست واقعی چیست؟
شکست واقعی؟
این است که بخواهی یک توسعهدهنده (Dev) باشی، اما هرگز شروع نکنی. 🚪
بیشتر مردم منتظر میمانند تا:
• آموزش "کامل" را پیدا کنند.
• دوره "درست" را پیدا کنند.
• وقت آزاد بیشتری داشته باشند.
اما Backend Engineering تنها با انجام دادن، آموخته میشود: 💻
• نوشتن اولین API خود.
• شکستن اولین شمای پایگاه داده خود.
• دیپلوی کردن چیزی که به سختی کار میکند.
این اشتباهات بخشی از فرآیند هستند. 🧩
اجتناب از آنها همان چیزی است که شما را عقب نگه میدارد. 🛑
آشفته شروع کن. کوچک شروع کن. فقط شروع کن. ✨
📩 پیامرسانی ساده در NET. با Redis Pub/Sub
ردیس یک انتخاب محبوب برای کش کردن دادهها است، اما قابلیتهای آن بسیار فراتر از این است. یکی از ویژگیهای کمتر شناخته شده آن، پشتیبانی از Pub/Sub است. کانالهای Redis یک رویکرد جذاب برای پیادهسازی پیامرسانی Real-time در برنامههای NET. شما ارائه میدهند. با این حال، همانطور که به زودی خواهید دید، کانالها دارای نقاط ضعفی نیز هستند. 🧐
💡 در این پست بررسی خواهیم کرد:
• اصول اولیه کانالهای Redis.
• موارد استفاده عملی برای کانالها.
• پیادهسازی یک مثال Pub/Sub در NET.
• ابطال کش (Cache Invalidation) در سیستمهای توزیع شده.
بیایید وارد جزئیات شویم! 👇
🌐 کانالهای ردیس (Redis Channels)
Redis channels
کانالهای ارتباطی نامگذاری شدهای هستند که الگوی پیامرسانی Publish/Subscribe را پیادهسازی میکنند. 🗣
هر کانال با یک نام منحصر به فرد (مانند notifications یا updates) شناسایی میشود. کانالها تحویل پیام از Publishers (ناشران) به Subscribers (مشترکین) را تسهیل میکنند.
Publishers
از دستور PUBLISH برای ارسال پیام به یک کانال خاص استفاده میکنند.
Subscribers
از دستور SUBSCRIBE برای ثبت علاقه به دریافت پیام از یک کانال استفاده میکنند.
🔄 مدل Topic-Based
کانالهای Redis از یک مدل Publish-Subscribe مبتنی بر Topic (موضوع) پیروی میکنند. چندین Publisher میتوانند به یک کانال پیام ارسال کنند و چندین Subscriber میتوانند پیامها را از آن کانال دریافت کنند.
⚠️ محدودیت کلیدی: عدم ذخیره پیام
با این حال، توجه به این نکته بسیار حیاتی است که کانالهای Redis پیامها را ذخیره نمیکنند. ❌ اگر هنگام انتشار یک پیام، هیچ مشترکی برای یک کانال وجود نداشته باشد، آن پیام بلافاصله دور ریخته میشود.
کانالهای Redis دارای معناشناسی "حداکثر یک بار تحویل" (at-most-once delivery) هستند.
📩 موارد استفاده عملی (Practical Use Cases)
با توجه به اینکه کانالهای Redis با معناشناسی "حداکثر یک بار تحویل" (at-most-once delivery) کار میکنند (پیامها در صورت عدم حضور مشترک ممکن است از دست بروند)، این کانالها برای سناریوهایی مناسب هستند که از دست دادن گاهبهگاه پیام قابل قبول بوده و ارتباط Real-time یا نزدیک به Real-time مطلوب است. ✅
🎯 چند مورد استفاده ممکن:
🔹️فیدهای شبکههای اجتماعی: انتشار پستها یا بهروزرسانیهای جدید برای کاربران. 📢
🔹️بهروزرسانی امتیازات زنده: ارسال امتیازات زنده بازیها یا بهروزرسانیهای ورزشی برای مشترکین. ⚽️
🔹️اپلیکیشنهای چت: تحویل پیامهای چت به صورت Real-time به شرکتکنندگان فعال. 💬
🔹️ویرایش مشارکتی: انتشار تغییرات در محیطهای ویرایش مشارکتی.
🔹️بهروزرسانیهای Distributed Cache: ابطال ورودیهای کش در سرورهای متعدد هنگام تغییر دادهها. (این مورد را در ادامه با جزئیات بیشتر پوشش خواهیم داد.)
💡نکته مهم: کانالهای Redis بهترین انتخاب برای دادههای حیاتی که از دست دادن پیام در آنها غیرقابل قبول است، نیستند. در چنین مواردی، باید یک سیستم پیامرسانی با قابلیت اطمینان بالاتر را در نظر بگیرید. 🔐
💻 پیادهسازی Pub/Sub با Redis Channels در NET.
ما برای ارسال پیامها با کانالهای Redis از کتابخانه StackExchange.Redis استفاده خواهیم کرد.
📥 نصب و راهاندازی
ابتدا آن را نصب میکنیم:
Install-Package StackExchange.Redis
شما میتوانید Redis را به صورت محلی در یک کانتینر Docker اجرا کنید. پورت پیشفرض 6379 است:
docker run -it -p 6379:6379 redis
📤 سرویس Producer (ناشر)
در اینجا یک سرویس Background ساده آمده است که به عنوان Producer (ناشر) پیامهای ما عمل میکند.
ما با اتصال به نمونه Redis خود یک ConnectionMultiplexer ایجاد میکنیم. این به ما امکان میدهد تا یک ISubscriber به دست آوریم که میتوانیم از آن برای پیامرسانی Pub/Sub استفاده کنیم. ISubscriber ما را قادر میسازد تا با تعیین نام کانال، یک پیام را در آن کانال منتشر کنیم.
public class Producer(ILogger<Producer> logger) : BackgroundService
{
private static readonly string ConnectionString = "localhost:6379";
private static readonly ConnectionMultiplexer Connection =
ConnectionMultiplexer.Connect(ConnectionString);
private const string Channel = "messages";
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var subscriber = Connection.GetSubscriber();
while (!stoppingToken.IsCancellationRequested)
{
var message = new Message(Guid.NewGuid(), DateTime.UtcNow);
var json = JsonSerializer.Serialize(message);
await subscriber.PublishAsync(Channel, json);
logger.LogInformation(
"Sending message: {Channel} - {@Message}",
message);
await Task.Delay(5000, stoppingToken);
}
}
}
حالا نوبت به معرفی یک سرویس Background مجزا برای مصرف پیامها میرسد. ⬇️
📩 پیامرسانی ساده در NET. با Redis Pub/Sub (ادامه)
📥 سرویس Consumer (مصرفکننده)
Consumer
به همان نمونه Redis متصل میشود و یک ISubscriber به دست میآورد. ISubscriber متد SubscribeAsync را در اختیار ما قرار میدهد که میتوانیم از آن برای اشتراک در دریافت پیام از یک کانال مشخص استفاده کنیم. این متد یک Callback Delegate را میپذیرد که میتوانیم از آن برای رسیدگی به پیام دریافتی استفاده کنیم. 🎧
public class Consumer(ILogger<Consumer> logger) : BackgroundService
{
private static readonly string ConnectionString = "localhost:6379";
private static readonly ConnectionMultiplexer Connection =
ConnectionMultiplexer.Connect(ConnectionString);
private const string Channel = "messages";
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var subscriber = Connection.GetSubscriber();
await subscriber.SubscribeAsync(Channel, (channel, message) =>
{
var message = JsonSerializer.Deserialize<Message>(message);
logger.LogInformation(
"Received message: {Channel} - {@Message}",
channel,
message);
});
}
}
💾 ابطال کش در سیستمهای توزیع شده (Cache Invalidation)
در یک پروژه اخیر، من یک چالش رایج در سیستمهای توزیع شده را حل کردم: همگام نگه داشتن کشها. 🧩 ما از یک رویکرد کشینگ دو سطحی استفاده میکردیم:
کش in-memory روی هر وب سرور برای دسترسی فوقسریع.
کش مشترک Redis برای جلوگیری از ضربه زدن مکرر به پایگاه داده.
مشکل این بود که وقتی دادهای در پایگاه داده تغییر میکرد، به راهی نیاز داشتیم تا به سرعت به تمام وب سرورها بگوییم که کشهای in-memory خود را پاک کنند. اینجا بود که Redis Pub/Sub به کمک آمد. ما یک کانال Redis را به طور خاص برای پیامهای ابطال کش راهاندازی کردیم. 📣
💻 پیادهسازی CacheInvalidationBackgroundService
هر برنامه یک سرویس CacheInvalidationBackgroundService را اجرا میکند که در کانال ابطال کش عضو میشود (Subscribe میکند):
public class CacheInvalidationBackgroundService(
IServiceProvider serviceProvider)
: BackgroundService
{
public const string Channel = "cache-invalidation";
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await subscriber.SubscribeAsync(Channel, (channel, key) =>
{
var cache = serviceProvider.GetRequiredService<IMemoryCache>();
cache.Remove(key);
return Task.CompletedTask;
});
}
}
هر زمان که دادهای در پایگاه داده تغییر میکند، ما یک پیام را در این کانال با کلید کش داده بهروز شده، منتشر میکنیم. تمام وب سرورها در این کانال عضو هستند، بنابراین فوراً از حذف داده قدیمی از کشهای in-memory خود مطلع میشوند. از آنجایی که کش in-memory در صورت از کار افتادن برنامه پاک میشود، از دست دادن پیامهای ابطال کش مشکلی ایجاد نمیکند. این کار کشهای ما را سازگار نگه میدارد و تضمین میکند که کاربران ما همیشه بهروزترین اطلاعات را میبینند. ✅
📝 خلاصه
Redis Pub/Sub
یک راهحل جادویی برای هر نیاز پیامرسانی نیست، اما سادگی و سرعت آن، آن را به یک ابزار ارزشمند تبدیل میکند. کانالها به ما امکان میدهند تا به راحتی ارتباط بین مؤلفههای با اتصال سست (loosely coupled components) را پیادهسازی کنیم. 🔗
کانالهای Redis دارای معناشناسی "حداکثر یک بار تحویل" (at-most-once delivery) هستند، بنابراین برای مواردی که از دست دادن گاهبهگاه پیام قابل قبول است، بهترین گزینه میباشند.
موفق باشید! 👋
🔖 هشتگها:
#Redis #PubSub #CacheInvalidation #DistributedSystems #DotNet #Messaging #BackgroundService
🌐 مروری بر HTTP
بدانیم که HTTP پروتکلی برای واکشی (fetching) منابعی مانند اسناد HTML است. این پروتکل، بنیان هرگونه تبادل داده در وب محسوب میشود و یک پروتکل کلاینت-سرور (Client-Server) است. این بدان معناست که درخواستها توسط گیرنده، که معمولاً مرورگر وب است، آغاز میشوند. 🖥 یک سند کامل معمولاً از منابعی مانند محتوای متنی، دستورالعملهای طرحبندی (layout)، تصاویر، ویدئوها، اسکریپتها و موارد دیگر ساخته میشود.
📬 تبادل پیامها (Messages)
کلاینتها (Clients) و سرورها (Servers) از طریق تبادل پیامهای مجزا با یکدیگر ارتباط برقرار میکنند (برخلاف یک جریان داده یا stream).
• پیامهایی که توسط کلاینت ارسال میشوند، درخواست (Requests) نامیده میشوند.
• پیامهایی که توسط سرور به عنوان پاسخ ارسال میشوند، پاسخ (Responses) نام دارند.
⚙️ ماهیت و تکامل HTTP
ماهیت HTTP که در اوایل دهه ۱۹۹۰ طراحی شد، یک پروتکل قابل توسعه (Extensible) است که در طول زمان تکامل یافته است.
این پروتکل یک پروتکل لایه کاربرد (Application Layer Protocol) است که روی TCP یا یک اتصال TCP رمزگذاری شده با TLS ارسال میشود، اگرچه از لحاظ نظری هر پروتکل انتقال قابل اعتماد دیگری میتواند استفاده شود.
به دلیل قابلیت توسعهپذیری، HTTP نه تنها برای واکشی hypertext documents، بلکه برای تصاویر و ویدئوها یا ارسال محتوا به سرورها (مانند نتایج فرمهای HTML) نیز استفاده میشود. همچنین میتوان از HTTP برای واکشی بخشهایی از اسناد استفاده کرد تا صفحات وب را در صورت نیاز بهروزرسانی کند. 🔄
📋 در این مقاله موارد زیر بررسی خواهند شد:
• اجزای سیستمهای مبتنی بر HTTP
• جنبههای اساسی HTTP
• آنچه با HTTP قابل کنترل است
• جریان HTTP (HTTP flow)
• پیامهای HTTP (HTTP Messages)
• و Api های مبتنی بر HTTP
🏗 اجزای سازنده سیستمهای مبتنی بر HTTP
گفتیم HTTP یک پروتکل کلاینت-سرور (Client-Server) است: درخواستها توسط یک موجودیت به نام User-agent (یا یک پروکسی به نمایندگی از آن) ارسال میشوند. 👤
اغلب اوقات،User-agent همان مرورگر وب است، اما میتواند هر چیزی باشد؛ برای مثال، یک ربات که وب را میخزد تا ایندکس موتور جستجو را پر کرده و نگهداری کند.
هر درخواست مجزا به یک سرور ارسال میشود، که آن را مدیریت کرده و پاسخی به نام پاسخ (Response) ارائه میدهد. بین کلاینت و سرور، موجودیتهای متعددی وجود دارند که در مجموع پروکسیها (Proxies) نامیده میشوند و عملیاتهای مختلفی را انجام میدهند و مثلاً به عنوان دروازه (Gateways) یا کشها (Caches) عمل میکنند. 🛡
⚠️ ملاحظات لایههای زیرین
در واقعیت، کامپیوترهای بیشتری بین یک مرورگر و سروری که درخواست را مدیریت میکند، وجود دارند: مسیریابها (Routers)، مودمها و غیره. به لطف طراحی لایهای وب، این موارد در لایههای Network و Transport پنهان هستند. HTTP در بالا، در Application Layer قرار دارد. اگرچه لایههای زیرین برای تشخیص مشکلات شبکه مهم هستند، اما در توصیف HTTP عمدتاً بیربط تلقی میشوند.
1️⃣ کلاینت: (User-agent)
گفتیم User-agent هر ابزاری است که از طرف کاربر عمل میکند. این نقش در درجه اول توسط مرورگر وب انجام میشود، اما همچنین ممکن است توسط برنامههایی که مهندسان و توسعهدهندگان وب برای Debug کردن برنامههای خود استفاده میکنند، ایفا شود. 🛠
مرورگر همیشه موجودیتی است که درخواست را آغاز میکند. این کار هرگز توسط سرور انجام نمیشود (اگرچه مکانیسمهایی در طول سالها برای شبیهسازی پیامهای آغاز شده توسط سرور اضافه شدهاند).
📖 فرایند نمایش صفحه
برای نمایش یک صفحه وب، مرورگر یک درخواست اولیه برای واکشی سند HTML که نمایانگر صفحه است، ارسال میکند. سپس این فایل را تجزیه (Parse) میکند و درخواستهای اضافی مربوط به اسکریپتهای اجرایی، اطلاعات طرحبندی (CSS) برای نمایش، و زیرمنابع موجود در صفحه (معمولاً تصاویر و ویدئوها) را ایجاد میکند. سپس مرورگر وب این منابع را ترکیب میکند تا سند کامل، یعنی صفحه وب را ارائه دهد. اسکریپتهایی که توسط مرورگر اجرا میشوند، میتوانند منابع بیشتری را در مراحل بعدی واکشی کنند و مرورگر صفحه وب را بر اساس آن بهروز میکند. 🔄
🔗 مفهوم Hypertext
یک صفحه وب یک سند Hypertext است. این بدان معنی است که برخی از بخشهای محتوای نمایش داده شده، لینک هستند که میتوانند فعال شوند (معمولاً با کلیک ماوس) تا یک صفحه وب جدید واکشی شود و به کاربر اجازه میدهد تا عامل کاربر خود را هدایت کرده و در وب ناوبری (Navigate) کند. مرورگر این دستورالعملها را به درخواستهای HTTP ترجمه میکند و پاسخهای HTTP را تفسیر میکند تا یک پاسخ واضح به کاربر ارائه دهد.
2️⃣ سرور وب (The Web Server)
در سمت مخالف کانال ارتباطی، سرور قرار دارد که سندی را که توسط کلاینت درخواست شده است، سرو (Serve) میکند. 💾
یک سرور از نظر مجازی فقط به عنوان یک ماشین واحد ظاهر میشود؛ اما در واقع ممکن است مجموعهای از سرورها باشد که بار را به اشتراک میگذارند (Load Balancing)، یا نرمافزارهای دیگری (مانند کشها، یک سرور پایگاه داده، یا سرورهای تجارت الکترونیک)، که سند را به طور کامل یا جزئی بر اساس درخواست تولید میکنند.
نکته: یک سرور لزوماً یک ماشین واحد نیست، بلکه چندین نمونه نرمافزار سرور میتوانند روی یک ماشین میزبانی شوند. با HTTP/1.1 و هدر Host، حتی ممکن است آدرس IP یکسانی داشته باشند. 🏠
3️⃣ پروکسیها (Proxies)
بین مرورگر وب و سرور، کامپیوترها و ماشینهای متعددی پیامهای HTTP را منتقل میکنند. به دلیل ساختار لایهای پشته وب، اکثر اینها در لایههای انتقال، شبکه یا فیزیکی عمل میکنند و در لایه HTTP شفاف میشوند و به طور بالقوه تأثیر قابل توجهی بر عملکرد دارند. ⚙️
آنهایی که در لایههای کاربرد عمل میکنند، عموماً پروکسیها نامیده میشوند. این پروکسیها میتوانند شفاف (Transparent) باشند و درخواستهایی را که دریافت میکنند بدون هیچ تغییری به جلو ارسال کنند، یا غیرشفاف (Non-transparent) باشند، که در این صورت قبل از ارسال درخواست به سرور، آن را به نحوی تغییر خواهند داد.
🎯 وظایف پروکسیها
پروکسیها ممکن است عملکردهای متعددی را انجام دهند:
🔹️کشینگ (Caching): (کش میتواند عمومی یا خصوصی باشد، مانند کش مرورگر)
🔹️فیلترینگ (Filtering): (مانند اسکن آنتیویروس یا کنترل والدین) 🛡
🔹️توزیع بار (Load Balancing): (برای اجازه دادن به سرورهای متعدد برای ارائه درخواستهای مختلف)
🔹️احراز هویت (Authentication): (برای کنترل دسترسی به منابع مختلف)
🔹️ثبت وقایع (Logging): (اجازه ذخیره اطلاعات تاریخی) 📜
🔑 جنبههای اساسی HTTP
به طور کلی طوری طراحی شده است که برای انسانها قابل خواندن باشد، حتی با پیچیدگیهای اضافه شده در HTTP/2 از طریق کپسولهسازی پیامهای HTTP در Frameها. 📖
پیامهای HTTP را میتوان توسط انسان خواند و درک کرد، که آزمایش را برای توسعهدهندگان آسانتر کرده و پیچیدگی را برای تازهواردها کاهش میدهد.
با معرفی HTTP Headers در HTTP/1.0، این پروتکل به راحتی قابل توسعه و آزمایش است. 🛠 عملکرد جدید حتی میتواند از طریق توافق بین یک کلاینت و یک سرور در مورد معناشناسی (semantics) یک هدر جدید معرفی شود.
اما در حالی که هسته HTTP خودش بدون وضعیت است، کوکیهای HTTP اجازه استفاده از Sessionهای با وضعیت (Stateful Sessions) را میدهند. با استفاده از قابلیت توسعه هدر، کوکیهای HTTP به گردش کار اضافه میشوند و امکان ایجاد Session در هر درخواست HTTP را برای به اشتراک گذاشتن زمینه (Context) یا همان وضعیت (State) فراهم میکنند. 🍪
اتصال در لایه انتقال کنترل میشود و بنابراین اساساً خارج از حوزه HTTP است. HTTP نیاز ندارد که پروتکل انتقال زیرین مبتنی بر اتصال باشد؛ بلکه تنها نیازمند این است که قابل اعتماد (reliable) باشد، یا پیامها را از دست ندهد (حداقل در چنین مواردی خطا ارائه دهد).
از بین دو پروتکل انتقال رایج در اینترنت، TCP قابل اعتماد است و UDP نیست. بنابراین HTTP بر استاندارد TCP تکیه دارد که مبتنی بر اتصال است. 🌐
قبل از اینکه یک کلاینت و سرور بتوانند یک جفت درخواست/پاسخ HTTP را تبادل کنند، باید یک اتصال TCP ایجاد کنند، فرآیندی که نیاز به چندین رفت و برگشت (round-trips) دارد. 🐢
رفتار پیشفرض HTTP/1.0 این بود که یک اتصال TCP مجزا برای هر جفت درخواست/پاسخ HTTP باز کند. این کار در هنگام ارسال درخواستهای متعدد در فاصله زمانی کوتاه، کارایی کمتری دارد.
برای کاهش این نقص، HTTP/1.1 پایپلاینینگ (pipelining) (که پیادهسازی آن دشوار بود) و اتصالات پایدار (persistent connections) را معرفی کرد: اتصال TCP زیرین میتواند با استفاده از هدر Connection تا حدی کنترل شود.
بعد HTTP/2 یک گام فراتر رفت و با Multiplexing پیامها روی یک اتصال واحد، به گرم نگه داشتن اتصال و کارایی بیشتر کمک کرد.
آزمایشهایی برای طراحی یک پروتکل انتقال بهتر که مناسبتر برای HTTP باشد، در حال انجام است. برای مثال، Google در حال آزمایش QUIC است که بر پایه UDP ساخته شده تا یک پروتکل انتقال قابل اعتمادتر و کارآمدتر را فراهم کند.
1️⃣کلا HTTP ساده است (HTTP is simple)
به طور کلی طوری طراحی شده است که برای انسانها قابل خواندن باشد، حتی با پیچیدگیهای اضافه شده در HTTP/2 از طریق کپسولهسازی پیامهای HTTP در Frameها. 📖
پیامهای HTTP را میتوان توسط انسان خواند و درک کرد، که آزمایش را برای توسعهدهندگان آسانتر کرده و پیچیدگی را برای تازهواردها کاهش میدهد.
2️⃣ HTTP قابل توسعه است (HTTP is extensible)
با معرفی HTTP Headers در HTTP/1.0، این پروتکل به راحتی قابل توسعه و آزمایش است. 🛠 عملکرد جدید حتی میتواند از طریق توافق بین یک کلاینت و یک سرور در مورد معناشناسی (semantics) یک هدر جدید معرفی شود.
3️⃣ و HTTP بدون وضعیت است، اما بدون Session نیست (HTTP is stateless, but not sessionless)بدون وضعیت (Stateless) است: هیچ ارتباطی بین دو درخواستی که به طور متوالی روی یک اتصال انجام میشوند، وجود ندارد.
اما در حالی که هسته HTTP خودش بدون وضعیت است، کوکیهای HTTP اجازه استفاده از Sessionهای با وضعیت (Stateful Sessions) را میدهند. با استفاده از قابلیت توسعه هدر، کوکیهای HTTP به گردش کار اضافه میشوند و امکان ایجاد Session در هر درخواست HTTP را برای به اشتراک گذاشتن زمینه (Context) یا همان وضعیت (State) فراهم میکنند. 🍪
4️⃣حالا نوبت HTTP و اتصالات (HTTP and connections)
اتصال در لایه انتقال کنترل میشود و بنابراین اساساً خارج از حوزه HTTP است. HTTP نیاز ندارد که پروتکل انتقال زیرین مبتنی بر اتصال باشد؛ بلکه تنها نیازمند این است که قابل اعتماد (reliable) باشد، یا پیامها را از دست ندهد (حداقل در چنین مواردی خطا ارائه دهد).
از بین دو پروتکل انتقال رایج در اینترنت، TCP قابل اعتماد است و UDP نیست. بنابراین HTTP بر استاندارد TCP تکیه دارد که مبتنی بر اتصال است. 🌐
⚙️ بهینهسازی اتصالات
قبل از اینکه یک کلاینت و سرور بتوانند یک جفت درخواست/پاسخ HTTP را تبادل کنند، باید یک اتصال TCP ایجاد کنند، فرآیندی که نیاز به چندین رفت و برگشت (round-trips) دارد. 🐢
رفتار پیشفرض HTTP/1.0 این بود که یک اتصال TCP مجزا برای هر جفت درخواست/پاسخ HTTP باز کند. این کار در هنگام ارسال درخواستهای متعدد در فاصله زمانی کوتاه، کارایی کمتری دارد.
برای کاهش این نقص، HTTP/1.1 پایپلاینینگ (pipelining) (که پیادهسازی آن دشوار بود) و اتصالات پایدار (persistent connections) را معرفی کرد: اتصال TCP زیرین میتواند با استفاده از هدر Connection تا حدی کنترل شود.
بعد HTTP/2 یک گام فراتر رفت و با Multiplexing پیامها روی یک اتصال واحد، به گرم نگه داشتن اتصال و کارایی بیشتر کمک کرد.
آزمایشهایی برای طراحی یک پروتکل انتقال بهتر که مناسبتر برای HTTP باشد، در حال انجام است. برای مثال، Google در حال آزمایش QUIC است که بر پایه UDP ساخته شده تا یک پروتکل انتقال قابل اعتمادتر و کارآمدتر را فراهم کند.
🔁 جریان HTTP :
هنگامی که یک کلاینت میخواهد با یک سرور (چه سرور نهایی و چه یک پروکسی میانی) ارتباط برقرار کند، مراحل زیر را طی میکند: 👇
1️⃣ باز کردن اتصال TCP
اتصال TCP برای ارسال یک یا چند درخواست و دریافت پاسخ استفاده میشود. کلاینت ممکن است یک اتصال جدید باز کند، از یک اتصال موجود مجدداً استفاده کند، یا چندین اتصال TCP به سرورها باز کند. 🔗
2️⃣ ارسال پیام HTTP
پیامهای HTTP (قبل از HTTP/2) قابل خواندن توسط انسان هستند. در HTTP/2، این پیامها در Frameها کپسولهسازی میشوند که خواندن مستقیم آنها را غیرممکن میسازد، اما اصل کار یکسان باقی میماند.
📌مثال درخواست (Request):
GET / HTTP/1.1
Host: developer.mozilla.org
Accept-Language: fr
3️⃣ خواندن پاسخ سرور
پاسخ ارسال شده توسط سرور خوانده میشود، مانند:
HTTP/1.1 200 OK
Date: Sat, 09 Oct 2010 14:28:02 GMT
Server: Apache
Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
ETag: "51142bc1-7449-479b075b2891b"
Accept-Ranges: bytes
Content-Length: 29769
Content-Type: text/html
<!doctype html>… (در اینجا 29769 بایت از صفحه وب درخواستی قرار میگیرد)
4️⃣ بستن یا استفاده مجدد از اتصال
اتصال برای درخواستهای بعدی بسته یا مجدداً استفاده میشود. 🔄
⚠️ ملاحظه Pipelining
اگر پایپلاینینگ HTTP فعال باشد، چندین درخواست میتوانند بدون انتظار برای دریافت کامل اولین پاسخ ارسال شوند. با این حال، پیادهسازی پایپلاینینگ HTTP در شبکههای موجود که بخشهای قدیمی نرمافزار با نسخههای مدرن همزیستی دارند، دشوار است. پایپلاینینگ HTTP در HTTP/2 با Multiplexing قویتر درخواستها در یک Frame جایگزین شده است.
📝 پیامهای HTTP (HTTP Messages)
پیامهای HTTP، همانطور که در HTTP/1.1 و نسخههای قبلی تعریف شدهاند، قابل خواندن توسط انسان هستند. در HTTP/2، این پیامها در یک ساختار دودویی، یعنی Frame، جاسازی میشوند که امکان بهینهسازیهایی مانند فشردهسازی هدرها و Multiplexing را فراهم میکند. حتی اگر تنها بخشی از پیام اصلی HTTP در این نسخه ارسال شود، معناشناسی هر پیام بدون تغییر باقی میماند و کلاینت درخواست اصلی HTTP/1.1 را (به صورت مجازی) بازسازی میکند. بنابراین، درک پیامهای HTTP/2 در قالب HTTP/1.1 مفید است.
دو نوع پیام HTTP وجود دارد: درخواستها (Requests) و پاسخها (Responses) که هر کدام قالب خاص خود را دارند.
🔹️ درخواستها (Requests) 📤
درخواستها شامل عناصر زیر هستند:
• متد HTTP: معمولاً یک فعل مانند GET، POST یا یک اسم مانند OPTIONS یا HEAD که عملیاتی را که کلاینت میخواهد انجام دهد، تعریف میکند. به طور معمول، کلاینت میخواهد یک منبع را واکشی کند (با استفاده از GET) یا مقدار یک فرم HTML را ارسال کند (با استفاده از POST).
• مسیر منبع: URL منبع که عناصر واضح از زمینه، مانند پروتکل (http://)، دامنه (مانند developer.mozilla.org) یا پورت TCP (مانند 80) از آن حذف شده است.
• نسخه پروتکل HTTP.
• هدرها (Headers) اختیاری: که اطلاعات اضافی را برای سرورها منتقل میکنند.
• بدنه (Body): برای برخی متدها مانند POST، مشابه پاسخها، که منبع ارسال شده را شامل میشود.
🔹️ پاسخها (Responses) 📥
پاسخها شامل عناصر زیر هستند:
• نسخه پروتکل HTTP: که از آن پیروی میکنند.
• کد وضعیت (Status Code): نشان میدهد که آیا درخواست موفقیتآمیز بوده یا خیر، و چرا. 🔢
• پیام وضعیت (Status Message): یک توضیح کوتاه غیررسمی از کد وضعیت.
• هدرهای HTTP: مانند هدرهای درخواستها.
• بدنه (Body) اختیاری: که منبع واکشی شده را شامل میشود.
📝 نتیجهگیری
فهمیدیم HTTP یک پروتکل قابل توسعه (Extensible) است که استفاده از آن آسان میباشد. ساختار کلاینت-سرور، همراه با قابلیت افزودن هدرها، به HTTP اجازه میدهد تا همگام با قابلیتهای گسترده وب پیشرفت کند. 📈
اگرچه HTTP/2 با جاسازی پیامهای HTTP در Frameها برای بهبود عملکرد، مقداری پیچیدگی اضافه میکند، ساختار اصلی پیامها از زمان HTTP/1.0 یکسان باقی مانده است. جریان Session همچنان اساسی است و امکان بررسی و Debug کردن آن را با یک HTTP network monitor فراهم میکند. 🔍
🔖 هشتگها:
#HTTP #RequestResponse #Networking #CORS #WebSecurity #Session #TCP #QUIC
🧠 Options Pattern Validation در ASP.NET Core با FluentValidation
اگر با Options Pattern در ASP.NET Core کار کرده باشید، احتمالاً با اعتبارسنجی داخلی DataAnnotations آشنا هستید.
هرچند این روش ساده و کاربردیست، اما وقتی نوبت به سناریوهای پیچیدهتر میرسد، محدودیتهای خودش را نشان میدهد.
📦 Options Pattern
به شما اجازه میدهد تا مقادیر تنظیمات (configuration) را بهصورت کلاسهای strongly-typed در زمان اجرا دریافت کنید.
اما یک مشکل وجود دارد:
🔸 شما نمیتوانید مطمئن باشید که مقادیر پیکربندی واقعاً معتبر هستند تا زمانیکه از آنها استفاده کنید!
راهحل؟
✅ اعتبارسنجی تنظیمات در زمان راهاندازی (Startup) برنامه.
در این پست یاد میگیریم چطور با ترکیب FluentValidation و Options Pattern،
یک سیستم اعتبارسنجی قدرتمند بسازیم که در startup اجرا میشود و از بروز خطاهای پیکربندی جلوگیری میکند.
⚖️ چرا FluentValidation بهتر از DataAnnotations است؟
در حالیکه DataAnnotations برای اعتبارسنجیهای ساده کافی است،FluentValidation امکانات بیشتری ارائه میدهد:
✨ قوانین اعتبارسنجی انعطافپذیرتر و خواناتر
⚙️ پشتیبانی از شرایط پیچیده و شرطی
🧩 جداسازی تمیز منطق اعتبارسنجی از مدلها
🧪 امکان تست سادهی قوانین
🧠 پشتیبانی بهتر از منطق سفارشی
🔌 قابلیت تزریق وابستگیها در Validatorها
⚙️ درک چرخهٔ حیات Options Pattern در ASP.NET Core
قبل از اینکه وارد جزئیات اعتبارسنجی بشیم، لازمه چرخهٔ حیات (Lifecycle) گزینهها در ASP.NET Core رو درک کنیم 👇
1️⃣ گزینهها در Container وابستگی (DI Container) ثبت میشن.
2️⃣ مقادیر پیکربندی (Configuration Values) به کلاسهای Options متصل میشن.
3️⃣ اگر اعتبارسنجی پیکربندی شده باشه، در این مرحله اجرا میشه.
4️⃣ گزینهها هنگام فراخوانی از طریق یکی از این اینترفیسها resolve میشن:
• IOptions<T>
• IOptionsSnapshot<T>
• IOptionsMonitor<T>
🧩 متد ()ValidateOnStart باعث میشه اعتبارسنجی در زمان راهاندازی (Startup) برنامه انجام بشه،
نه وقتی که برای اولین بار Options مورد استفاده قرار میگیرن.
⚠️ خطاهای رایج پیکربندی بدون اعتبارسنجی
بدون اعتبارسنجی، مشکلات پیکربندی میتوانند به چند شکل ظاهر شوند:
خطاهای خاموش (Silent failures): یک گزینهی پیکربندی اشتباه ممکن است باعث شود مقادیر پیشفرض بدون هیچ هشداری استفاده شوند.
🔹️ استثناهای زمان اجرا (Runtime exceptions): مشکلات پیکربندی ممکن است تنها وقتی که برنامه تلاش میکند مقادیر نامعتبر را استفاده کند، ظاهر شوند.
🔹️ خطاهای زنجیرهای (Cascading failures): یک کامپوننت پیکربندیشده نادرست میتواند باعث شکست سیستمهای وابسته شود.
با اعتبارسنجی در زمان Startup برنامه، یک چرخه بازخورد سریع ایجاد میکنید که از بروز این مشکلات جلوگیری میکند. ✅
🧱 تنظیم پایهها (Setting Up the Foundation)
اول، پکیج FluentValidation را به پروژه اضافه میکنیم:
Install-Package FluentValidation # پکیج اصلی
Install-Package FluentValidation.DependencyInjectionExtensions # برای یکپارچگی با DI
در مثال ما، از یک کلاس تنظیمات به نام GitHubSettings استفاده میکنیم که نیاز به اعتبارسنجی دارد:
public class GitHubSettings
{
public const string ConfigurationSection = "GitHubSettings";
public string BaseUrl { get; init; }
public string AccessToken { get; init; }
public string RepositoryName { get; init; }
}
🧩 ایجاد Validator با FluentValidation
سپس یک Validator برای کلاس تنظیمات میسازیم:
public class GitHubSettingsValidator : AbstractValidator<GitHubSettings>
{
public GitHubSettingsValidator()
{
RuleFor(x => x.BaseUrl).NotEmpty();
RuleFor(x => x.BaseUrl)
.Must(baseUrl => Uri.TryCreate(BaseUrl, UriKind.Absolute, out _))
.When(x => !string.IsNullOrWhiteSpace(x.BaseUrl))
.WithMessage($"{nameof(GitHubSettings.BaseUrl)} must be a valid URL");
RuleFor(x => x.AccessToken)
.NotEmpty();
RuleFor(x => x.RepositoryName)
.NotEmpty();
}
}
🏗 ساخت یکپارچهسازی FluentValidation
برای ادغام FluentValidation با Options Pattern، نیاز داریم یک پیادهسازی سفارشی از <IValidateOptions<T بسازیم:
using FluentValidation;
using Microsoft.Extensions.Options;
public class FluentValidateOptions<TOptions>
: IValidateOptions<TOptions>
where TOptions : class
{
private readonly IServiceProvider _serviceProvider;
private readonly string? _name;
public FluentValidateOptions(IServiceProvider serviceProvider, string? name)
{
_serviceProvider = serviceProvider;
_name = name;
}
public ValidateOptionsResult Validate(string? name, TOptions options)
{
if (_name is not null && _name != name)
{
return ValidateOptionsResult.Skip;
}
ArgumentNullException.ThrowIfNull(options);
using var scope = _serviceProvider.CreateScope();
var validator = scope.ServiceProvider.GetRequiredService<IValidator<TOptions>>();
var result = validator.Validate(options);
if (result.IsValid)
{
return ValidateOptionsResult.Success;
}
var type = options.GetType().Name;
var errors = new List<string>();
foreach (var failure in result.Errors)
{
errors.Add($"Validation failed for {type}.{failure.PropertyName} " +
$"with the error: {failure.ErrorMessage}");
}
return ValidateOptionsResult.Fail(errors);
}
}
📝 نکات مهم درباره این پیادهسازی
یک scoped service provider ساخته میشود تا Validator به درستی Resolve شود (چون Validatorها معمولاً به صورت Scoped ثبت میشوند).
گزینههای دارای نام با استفاده از پراپرتی _name مدیریت میشوند.
پیامهای خطای دقیق و اطلاعاتی ساخته میشوند که شامل نام پراپرتی و پیام خطا هستند.
⚙️ نحوه عملکرد یکپارچهسازی FluentValidation
• اینترفیس <IValidateOptions<T نقطه اتصال ASP.NET Core برای اعتبارسنجی Options است.
• کلاس <FluentValidateOptions<T این اینترفیس را پیادهسازی میکند تا ارتباط با FluentValidation برقرار شود.
• وقتی ()ValidateOnStart فراخوانی میشود، ASP.NET Core همه پیادهسازیهای <IValidateOptions<T را Resolve کرده و اجرا میکند.
• اگر اعتبارسنجی شکست بخورد، OptionsValidationException پرتاب میشود و از شروع برنامه جلوگیری میکند. ✅
🛠 ساخت متدهای Extension برای ادغام آسان
حالا بیایید چند extension method بسازیم تا استفاده از FluentValidation در Options Pattern راحتتر شود:
public static class OptionsBuilderExtensions
{
public static OptionsBuilder<TOptions> ValidateFluentValidation<TOptions>(
this OptionsBuilder<TOptions> builder)
where TOptions : class
{
builder.Services.AddSingleton<IValidateOptions<TOptions>>(
serviceProvider => new FluentValidateOptions<TOptions>(
serviceProvider,
builder.Name));
return builder;
}
}
این extension method به ما امکان میدهد هنگام کانفیگ Options، متد ()ValidateFluentValidation را فراخوانی کنیم، مشابه متد داخلی ()ValidateDataAnnotations.
برای راحتی بیشتر، میتوانیم یک extension method دیگر بسازیم تا کل فرآیند کانفیگ را ساده کند:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddOptionsWithFluentValidation<TOptions>(
this IServiceCollection services,
string configurationSection)
where TOptions : class
{
services.AddOptions<TOptions>()
.BindConfiguration(configurationSection)
.ValidateFluentValidation() // کانفیگ FluentValidation
.ValidateOnStart(); // اعتبارسنجی در هنگام شروع برنامه
return services;
}
}
📝 ثبت و استفاده از اعتبارسنجی
چند روش برای استفاده از این ادغام FluentValidation وجود دارد:
1️⃣: ثبت استاندارد با ثبت دستی Validator
// ثبت Validator
builder.Services.AddScoped<IValidator<GitHubSettings>, GitHubSettingsValidator>();
// کانفیگ Options با اعتبارسنجی
builder.Services.AddOptions<GitHubSettings>()
.BindConfiguration(GitHubSettings.ConfigurationSection)
.ValidateFluentValidation() // فعال کردن FluentValidation
.ValidateOnStart();
2️⃣: استفاده از Extension راحت
// ثبت Validator
builder.Services.AddScoped<IValidator<GitHubSettings>, GitHubSettingsValidator>();
// استفاده از Extension راحت
builder.Services.AddOptionsWithFluentValidation<GitHubSettings>(GitHubSettings.ConfigurationSection);
3️⃣: ثبت خودکار Validatorها
اگر Validatorهای زیادی دارید و میخواهید همه را یکجا ثبت کنید، میتوانید از assembly scanning استفاده کنید:
// ثبت همه Validatorها از assembly
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
// استفاده از Extension راحت
builder.Services.AddOptionsWithFluentValidation<GitHubSettings>(GitHubSettings.ConfigurationSection);
⚡️ اتفاقات هنگام اجرای برنامه
با ()ValidateOnStart، اگر هر قانونی نقض شود، برنامه هنگام startup یک استثنا پرتاب میکند.
مثال: اگر در appsettings.json مقدار AccessToken موجود نباشد، پیام خطا شبیه زیر خواهد بود:
Microsoft.Extensions.Options.OptionsValidationException:
Validation failed for GitHubSettings.AccessToken with the error: 'Access Token' must not be empty.
این کار از شروع برنامه با پیکربندی اشتباه جلوگیری میکند و اطمینان حاصل میکند که مشکلات در اوایل چرخه توسعه شناسایی شوند. ✅
⚙️ کار با منابع مختلف کانفیگ
سیستم کانفیگ ASP.NET Core از چندین منبع پشتیبانی میکند. هنگام استفاده از Options Pattern با FluentValidation، به یاد داشته باشید که اعتبارسنجی صرفنظر از منبع کار میکند:
🔹️متغیرهای محیطی (Environment variables)
🔹️Azure Key Vault
🔹️User secrets
🔹️فایلهای JSON
🔹️کانفیگ در حافظه (In-memory configuration)
این ویژگی مخصوصاً برای برنامههای containerized مفید است که کانفیگ از طریق متغیرهای محیطی یا secretهای mount شده میآید.
🧪 تست Validatorهای خود
یکی از مزایای استفاده از FluentValidation این است که Validatorها بسیار آسان تست میشوند:
// استفاده از متدهای کمکی FluentValidation.TestHelper
[Fact]
public void GitHubSettings_WithMissingAccessToken_ShouldHaveValidationError()
{
// Arrange
var validator = new GitHubSettingsValidator();
var settings = new GitHubSettings { RepositoryName = "test-repo" };
// Act
TestValidationResult<GitHubSettings>? result = await validator.TestValidate(settings);
// Assert
result.ShouldNotHaveAnyValidationErrors();
}
✅ خلاصه
با ترکیب FluentValidation با Options Pattern و ()ValidateOnStart، یک سیستم اعتبارسنجی قدرتمند ایجاد میکنیم که تضمین میکند کانفیگ برنامه درست باشد از همان ابتدا.
مزایای این روش:
• قوانین اعتبارسنجی بیانگراتر و انعطافپذیرتر نسبت به Data Annotations
• جداسازی منطق اعتبارسنجی از مدلهای کانفیگ
• کشف خطاهای کانفیگ در زمان startup برنامه
• پشتیبانی از سناریوهای اعتبارسنجی پیچیده
• قابلیت تست آسان
این الگو به ویژه در معماریهای میکروسرویس یا برنامههای containerized ارزشمند است، جایی که خطاهای کانفیگ باید فوراً تشخیص داده شوند و نه در زمان اجرا.
به یاد داشته باشید که Validatorهای خود را به درستی ثبت کنید و از ()ValidateOnStart استفاده کنید تا اعتبارسنجی در زمان شروع برنامه اجرا شود.
🏷 هشتگها:
#ASPNetCore #FluentValidation #OptionsPattern #Validation