در این روش، وابستگیهای یک کلاس به صورت صریح (Explicit) از طریق Constructor اعلام میشوند. این پترن که به Constructor Injection معروف است، پایبند به اصل اول SOLID (یعنی Single Responsibility) و اصل Dependency Inversion است.
public class OrderService : IOrderService
{
private readonly IUserRepository _userRepository;
private readonly INotificationService _notificationService;
public OrderService(IUserRepository userRepository, INotificationService notificationService)
{
_userRepository = userRepository;
_notificationService = notificationService;
}
}
مکانیسم عملکرد:
زمانی که یک Request وارد پایپلاین فریمورک (مثلاً یک HTTP Request در ASP.NET Core) میشود، فریمورک خودش یک IServiceScope برای آن درخواست ایجاد میکند. تمام سرویسهای Scoped که به صورت مستقیم در Constructorها تزریق شدهاند، در طول این فرستاده (Request) یک Instance واحد دارند و در پایان عمر Request، فریمورک به صورت خودکار آنها را Dispose میکند.
در این رویکرد، کلاس ما به جای درخواست وابستگیهای واقعی، خودِ کانتینر یا همان IServiceProvider (یا IServiceScopeFactory) را تزریق میکند. سپس در بدنه کد، به صورت دستی اقدام به ایجاد یک طول عمر جدید (Nested Scope) و حل کردن (Resolve) سرویسها میکند.
public class OrderProcessingJob
{
private readonly IServiceScopeFactory _scopeFactory;
public OrderProcessingJob(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task ExecuteAsync()
{
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService();
// انجام عملیات با اینستنس مجزا
}
}
}
مکانیسم عملکرد:
با فراخوانی CreateScope()، یک گره جدید در درخت وابستگیها ایجاد میشود. هر سرویس Scoped که از این scope.ServiceProvider درخواست شود، یک Instance کاملاً جدید و ایزوله نسبت به Scope مادری (Outer Scope) خواهد بود. این Instance تنها تا زمانی زنده است که بلوک using پایان یابد.
برای انتخاب درست، باید متریکهای معماری هر دو روش را روی ترازوی سنجش قرار دهیم.
جدول مقایسه ساختاری Direct DI و Nested Scope
| معیار ارزیابی | تزریق مستقیم (Direct DI) | سرویس پرووایدر و نِستد اسکوپ (Nested Scope) |
| شفافیت وابستگیها | عالی (تزریق صریح در کانستراکتور) | ضعیف (وابستگیهای پنهان در بدنه متدها) |
| قابلیت تست واحد (Unit Testing) | بسیار ساده (با Mock کردن راحت وابستگیها) | پیچیده (نیازمند Mock کردن کل کانتینر یا پترنها) |
| مدیریت همزمانی (Concurrency) | چالشبرانگیز در تردهای موازی | عالی (ایزولاسیون کامل منابع در تردهای مجزا) |
| اورهد عملکردی (Performance) | بهینهترین حالت ممکن | دارای اورهد ساخت کانتینر فرزند و GC کارهای اضافه |
| امنیت تراکنشها (Transactions) | یکپارچه و هماهنگ | ریسک بالای عدم تطابق تراکنشها (Scope Mismatch) |
۱. عارضه Transaction Scope Mismatch
۲. پترن مخرب Service Locator (Anti-Pattern)
وقتی IServiceProvider را به یک کلاس پاس میدهی، امضای متد یا کانستراکتور کلاس دروغ میگوید! کلاس ادعا میکند "من فقط به سرویس پرووایدر نیاز دارم"، اما در واقعیت ممکن است در خط ۲۵۰ کد، متد GetRequiredService() را صدا بزند. این یعنی:
تستنویسی سخت: باید برای تست یک کلاس ساده، کل رفتارهای IServiceProvider را Mock کنی.
خطای زمان اجرا (Runtime Error): خطاهای عدم ثبت سرویس (Missing Registration) به جای زمان لود برنامه یا کامپایل، وسط کارکردن مشتری در محیط عملیاتی خود را نشان میدهند.
۳. اورهد عملکردی و نشت حافظه (Memory Leaks)
با وجود تمام این معایب، IServiceProvider و Nested Scopeها برای حل مسائلی حیاتی در معماری نرمافزار طراحی شدهاند که با Direct DI معمولی غیرقابل حل هستند. در ادامه سناریوهای طلایی استفاده از آنها را بررسی میکنیم:
یکی از رایجترین سناریوها، استفاده از IHostedService یا BackgroundService در داتنت است. طبق تعریف فریمورک، Hosted Serviceها به صورت Singleton ثبت میشوند (چون باید در کل طول عمر اپلیکیشن بالا بمانند).
مسئله: یک سرویس Singleton نمیتواند به صورت مستقیم یک سرویس Scoped (مثل DbContext یا دیتابیس کانکشن) را در کانستراکتور خود دریافت کند. این کار باعث خطای معروف Scoped Dependency Capturing میشود (چون یک موجودیت Singleton باعث میشود یک موجودیت Scoped تا ابد زنده بماند و این یعنی فاجعه همزمانی و مالتیتردینگ دیتابیس).
راهکار تکاملی: در اینجا چارهای جز ایجاد دستی اسکوپ وجود ندارد. شما IServiceScopeFactory را به صورت Singleton تزریق میکنی و در هر بازه زمانی (مثلاً هر ۱۰ ثانیه)، یک اسکوپ کوتاه مدت میسازی تا کارهای دیتابیسی را تر و تمیز انجام دهد و بسته شود:
public class QueueProcessorDailyJob : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public QueueProcessorDailyJob(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService();
// پردازش صف با دیتابیس کانکشن تازه و ایزوله
await dbContext.ProcessPendingOrdersAsync();
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
فرض کن در یک Request خصیصه پردازش دستهای (Bulk Processing) داری. میخواهی ۱۰۰ رکورد را به صورت همزمان و با استفاده از Parallel.ForEachAsync یا Task.WhenAll پردازش کنی تا سرعت سیستم بالا برود.
مسئله: ابزاری مثل DbContext در EF Core به هیچ وجه Thread-safe نیست. اگر چند Thread همزمان بخواهند از یک اینستنس تزریق شده به صورت مستقیم (Direct DI) استفاده کنند، برنامه بلافاصله کرش کرده و خطای مالتیتردینگ صادر میکند.
راهکار تکاملی: برای هر Thread یا هر تسک موازی، یک Nested Scope جداگانه ایجاد میکنی. با این کار، هر نخ، نسخه کاملاً ایزوله خود از دیتابیس کانکشن را دارد و هیچ تداخلی پیش نمیآید.
public async Task ProcessInParallelAsync(List orderIds)
{
await Parallel.ForEachAsync(orderIds, async (orderId, cancellationToken) =>
{
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService();
var orderService = scope.ServiceProvider.GetRequiredService();
await orderService.FinalizeOrderAsync(orderId, dbContext);
}
});
}
وقتی در حال توسعه ابزارهای زیرساختی (Infrastructure) مانند یک مفسر پیام (Message Broker Consumer مثل RabbitMQ / MassTransit)، یا فریمورکهای سکیوریتی و احراز هویت هستی، نیاز داری پیامهای ورودی را که خارج از اتمسفر HTTP Request سست کار میکنند، مدیریت کنی. وقتی پکت یا پیامی از صف دریافت میشود، فریمورک اصلی برای آن اسکوپ پیشفرضی ندارد. شما باید دستی یک اسکوپ بسازی تا پیام پردازش شده و تمام سرویسهای Scoped مرتبط با آن پیام، پس از اتمام کار آزاد شوند.
قوانین طلایی (Best Practices): کی استفاده کنیم، کی نکنیم؟
برای اینکه درگیر معماریهای غلط نشوی، این چکلیست را همیشه در ذهن داشته باش:
❌ هرگز از ServiceProvider استفاده نکن اگر:
حتماً و با دقت از Nested Scope استفاده کن اگر:
تزریق وابستگی مستقیم (Direct DI) پادشاه بیچونوچرای کدهای روزمره و بیزینسی ماست. این روش وضوح، قابلیت تست بالا و امنیت رفتاری سیستم را تضمین میکند.
سرویس پرووایدر و مکانیسم Nested Scope ابزارهای جراحی پیشرفتهای هستند. آنها برای سناریوهای خط مقدم وب طراحی نشدهاند، بلکه برای لایههای عمیق زیرساختی، پردازشهای ناهمگام، پسزمینه و مدیریت مالتیتردینگ پدید آمدهاند. استفاده نادرست از آنها کد شما را به ضد الگوی Service Locator آلوده میکند، اما استفاده هوشمندانه از آنها، کلید حل گرههای کورِ محدودیت طول عمر سرویسها در معماریهای بزرگ است. معماری خود را بر اساس شفافیت بنا کن و هرجا که نیاز به شکستن مرزهای استاندارد چرخه حیات داشتی، با رعایت اصول تمیزکاری و دیسپوز، به سراغ اسکوپهای تودرتو برو.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.