پادشاهِ کُدنویسا شو!
کینگتو - آموزش برنامه نویسی تخصصصی - دات نت - سی شارپ - بانک اطلاعاتی و امنیت

ServiceProvider چیست؟ کالبدشکافی مدیریت طول عمر وابستگی‌ها: Direct DI در برابر Nested Scope

8 بازدید 0 نظر ۱۴۰۵/۰۳/۱۴
بیا در این مقاله تخصصی، یک بار برای همیشه مرز بین تزریق وابستگی مستقیم (Direct DI) و ساخت Scopeهای تودرتو (Nested Scope / IServiceProvider) را شفاف کنیم، معماری پشت هرکدام را بشکافیم و ببینیم غول‌های نرم‌افزاری چطور و کجا از این ابزارها استفاده می‌کنند. در فریم‌ورک‌های مدرن توسعه نرم‌افزار (مانند .NET Core / ASP.NET Core)، سیستم Built-in IoC Container وظیفه مدیریت چرخه حیات (Lifecycle) سرویس‌ها را بر عهده دارد. برای درک تفاوت این دو رویکرد، ابتدا باید نگاهی عمیق به نحوه رفتار Container داشته باشیم.

تزریق وابستگی مستقیم (Direct Dependency Injection)

در این روش، وابستگی‌های یک کلاس به صورت صریح (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 می‌کند.

 

استفاده از ServiceProvider و ساخت Nested Scope

در این رویکرد، کلاس ما به جای درخواست وابستگی‌های واقعی، خودِ کانتینر یا همان 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 پایان یابد.

تحلیل عمیق مزایا و معایب (The Architectural Trade-offs)

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

جدول مقایسه ساختاری Direct DI و Nested Scope

معیار ارزیابی تزریق مستقیم (Direct DI) سرویس پرووایدر و نِستد اسکوپ (Nested Scope)
شفافیت وابستگی‌ها عالی (تزریق صریح در کانستراکتور) ضعیف (وابستگی‌های پنهان در بدنه متدها)
قابلیت تست واحد (Unit Testing) بسیار ساده (با Mock کردن راحت وابستگی‌ها) پیچیده (نیازمند Mock کردن کل کانتینر یا پترن‌ها)
مدیریت همزمانی (Concurrency) چالش‌برانگیز در ترد‌های موازی عالی (ایزولاسیون کامل منابع در تردهای مجزا)
اورهد عملکردی (Performance) بهینه‌ترین حالت ممکن دارای اورهد ساخت کانتینر فرزند و GC کارهای اضافه
امنیت تراکنش‌ها (Transactions) یکپارچه و هماهنگ ریسک بالای عدم تطابق تراکنش‌ها (Scope Mismatch)

 

بررسی دقیق‌تر معایب Nested Scope (تحقیقات شما در ترازوی نقد)

۱. عارضه Transaction Scope Mismatch

  • وقتی در محیطی مثل Entity Framework Core کار می‌کنی، DbContext به صورت Scoped ثبت می‌شود. اگر در الگوهای بیرونی یک تراکنش را با _context.Database.BeginTransactionAsync() شروع کنی و سپس داخل یک Nested Scope یک IDbContext جدید ایجاد کنی، این دو کانفیکت دارند. کانفیکت آن‌ها به این دلیل است که دو Object ID متفاوت در حافظه دارند و دو Connection مجزا به دیتابیس باز می‌کنند. تغییرات در Context دوم، هرگز توسط تراکنش اول Rollback یا Commit نمی‌شود؛ نتیجه؟ دیتا کلاپس و ناسازگاری شدید در دیتابیس.

۲. پترن مخرب Service Locator (Anti-Pattern)

وقتی IServiceProvider را به یک کلاس پاس می‌دهی، امضای متد یا کانستراکتور کلاس دروغ می‌گوید! کلاس ادعا می‌کند "من فقط به سرویس پرووایدر نیاز دارم"، اما در واقعیت ممکن است در خط ۲۵۰ کد، متد GetRequiredService() را صدا بزند. این یعنی:

  • تست‌نویسی سخت: باید برای تست یک کلاس ساده، کل رفتارهای IServiceProvider را Mock کنی.

  • خطای زمان اجرا (Runtime Error): خطاهای عدم ثبت سرویس (Missing Registration) به جای زمان لود برنامه‌ یا کامپایل، وسط کارکردن مشتری در محیط عملیاتی خود را نشان می‌دهند.

۳. اورهد عملکردی و نشت حافظه (Memory Leaks)

  • هر زمان که CreateScope() صدا زده می‌شود، آبجکت‌های مدیریتی زیادی در پشته حافظه ساخته می‌شوند. اگر این کار را داخل یک حلقه for با تعداد بالا انجام دهی، فرآیند Garbage Collection (GC) دچار گلوگاه شده و عملکرد برنامه به شدت افت می‌کند. علاوه بر این، اگر یادت برود اسکوپ ایجاد شده را Dispose کنی (مثلاً عدم استفاده از using یا عدم فراخوانی Dispose)، سرویس‌های ساخته شده تا پایان عمر پروسس اصلی در حافظه باقی می‌مانند و باعث Memory Leak می‌شوند.

 

پس اصلاً چرا این قابلیت وجود دارد؟ (کجا باید استفاده شود؟)

با وجود تمام این معایب، IServiceProvider و Nested Scopeها برای حل مسائلی حیاتی در معماری نرم‌افزار طراحی شده‌اند که با Direct DI معمولی غیرقابل حل هستند. در ادامه سناریوهای طلایی استفاده از آن‌ها را بررسی می‌کنیم:

 

عملیات‌های پس‌زمینه و کارهای زمان‌بندی شده (Background Tasks / Hosted Services)

یکی از رایج‌ترین سناریوها، استفاده از 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);
        }
    }
}

 

پردازش‌های موازی و چندنخی (Multi-Threading & Parallel Processing)

فرض کن در یک 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 Patterns)

وقتی در حال توسعه ابزارهای زیرساختی (Infrastructure) مانند یک مفسر پیام (Message Broker Consumer مثل RabbitMQ / MassTransit)، یا فریم‌ورک‌های سکیوریتی و احراز هویت هستی، نیاز داری پیام‌های ورودی را که خارج از اتمسفر HTTP Request سست کار می‌کنند، مدیریت کنی. وقتی پکت یا پیامی از صف دریافت می‌شود، فریم‌ورک اصلی برای آن اسکوپ پیش‌فرضی ندارد. شما باید دستی یک اسکوپ بسازی تا پیام پردازش شده و تمام سرویس‌های Scoped مرتبط با آن پیام، پس از اتمام کار آزاد شوند.

قوانین طلایی (Best Practices): کی استفاده کنیم، کی نکنیم؟

برای اینکه درگیر معماری‌های غلط نشوی، این چک‌لیست را همیشه در ذهن داشته باش:

❌ هرگز از ServiceProvider استفاده نکن اگر:

  1. در سطح کنترلرها (Controllers)، مینیمال ای‌پی‌آی‌ها (Minimal APIs) یا سرویس‌های معمولی اپلیکیشن (Application Services) هستی. در این لایه‌ها همیشه از Direct DI استفاده کن.
  2. هدف اصلی‌ات صرفاً «خلوت کردن کانستراکتور شلوغ کلاس» است! اگر کلاسی بیش از حد وابستگی دارد، نشانه نقص در طراحی آن است (باید به سرویس‌های کوچک‌تر شکسته شود، نه اینکه با سرویس پرووایدر کثیف‌کاری‌ها پنهان شود).
  3. می‌خواهی تراکنش‌های مالی یا دیتابیسیِ زنجیره‌ای و متصل به هم بزنی که شکست یکی باید کل فرآیند را برگرداند.

حتماً و با دقت از Nested Scope استفاده کن اگر:

  1. در حال نوشتن یک BackgroundService یا IHostedService مانا هستی و نیاز به مصرف سرویس‌های غیر Singleton داری.
  2. نیاز به موازی‌سازی تسک‌هایی داری که وابستگی‌های غیر Thread-Safe (مثل پایگاه داده) دارند.
  3. در حال طراحی مدولار یا ساخت افزونه (Plugin Architecture) هستی که در زمان اجرا، باید کامپوننت‌های ناشناخته و پویا را بارگذاری کند.
  4. مدیریت چرخه حیات دقیق یک شیء برای شما حیاتی است؛ مثلاً می‌خواهی یک سرویس Scoped سنگین فوراً پس از انجام کارش از حافظه حذف شود و منتظر پایان عمر Request اصلی نماند.

 

جمع‌بندی و نتیجه‌گیری معماری

تزریق وابستگی مستقیم (Direct DI) پادشاه بی‌چون‌وچرای کدهای روزمره و بیزینسی ماست. این روش وضوح، قابلیت تست بالا و امنیت رفتاری سیستم را تضمین می‌کند.

سرویس پرووایدر و مکانیسم Nested Scope ابزارهای جراحی پیشرفته‌ای هستند. آن‌ها برای سناریوهای خط مقدم وب طراحی نشده‌اند، بلکه برای لایه‌های عمیق زیرساختی، پردازش‌های ناهمگام، پس‌زمینه و مدیریت مالتی‌تردینگ پدید آمده‌اند. استفاده نادرست از آن‌ها کد شما را به ضد الگوی Service Locator آلوده می‌کند، اما استفاده هوشمندانه از آن‌ها، کلید حل گره‌های کورِ محدودیت طول عمر سرویس‌ها در معماری‌های بزرگ است. معماری خود را بر اساس شفافیت بنا کن و هرجا که نیاز به شکستن مرزهای استاندارد چرخه حیات داشتی، با رعایت اصول تمیزکاری و دیسپوز، به سراغ اسکوپ‌های تودرتو برو.

 
لینک استاندارد شده: GnMWXxYK

0 نظر

    هنوز نظری برای این مقاله ثبت نشده است.
جستجوی مقاله و آموزش
دوره‌ها با تخفیفات ویژه