مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت سوم - رهاسازی منابع سرویس‌های IDisposable
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هفت دقیقه

یکی از پرکاربردترین اینترفیس‌های NET.، اینترفیس IDisposable است. عموما کلاس‌هایی که ارجاعی را به منابع غیر مدیریت شده مانند فایل‌ها و سوکت‌ها داشته باشند، این اینترفیس را پیاده سازی می‌کنند. garbage collector به صورت خودکار حافظه‌ی اشیاء مدیریت شده یا دات نتی را رها می‌کند؛ اما چیزی را در مورد منابع غیر مدیریت شده نمی‌داند. به همین جهت پیاده سازی اینترفیس IDisposable روشی را جهت پاکسازی این منابع به garbage collector معرفی می‌کند.


رفتار IoC Container توکار ASP.NET Core با سرویس‌های IDisposable

ASP.NET Core به همراه یک IoC Container توکار ارائه می‌شود و اگر سرویسی با طول عمرTransient و یا Scoped به آن معرفی شود و همچنین این سرویس اینترفیس IDisposable را نیز پیاده سازی کند، کار dispose خودکار آن در پایان درخواست جاری صورت می‌گیرد و نیازی به تنظیمات اضافه‌تری ندارد. در اینجا سرویس‌هایی با طول عمر Singleton نیز در پایان کار برنامه، زمانیکه خود ServiceProvider به پایان کارش می‌رسد، dispose خواهند شد.
البته این مورد یک شرط را نیز به همراه دارد: کار وهله سازی سرویس‌های درخواستی باید توسط خود این IoC Container مدیریت شود تا در پایان کار بداند چگونه آن‌ها را Dispose کند.

یک مثال: بررسی Dispose شدن خودکار یک سرویس IDisposable
namespace CoreIocServices
{
    public interface IMyDisposableService
    {
        void Run();
    }

    public class MyDisposableService : IMyDisposableService, IDisposable
    {
        private readonly ILogger<MyDisposableService> _logger;

        public MyDisposableService(ILogger<MyDisposableService> logger)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _logger.LogInformation("+ {0} was created", this.GetType().Name);
        }

        public void Run()
        {
            _logger.LogInformation("Running MyDisposableService!");
        }

        public void Dispose()
        {
            _logger.LogInformation("- {0} was disposed!", this.GetType().Name);
        }
    }
}
سرویس ساده‌ی فوق، اینترفیس IDisposable را پیاده سازی می‌کند و با استفاده از ILogger، پیام‌هایی را در زمان ایجاد و Dipose آن در پنجره کنسول و یا دیباگ نمایش خواهد داد.
اگر این سرویس را به یک برنامه‌ی ASP.NET Core معرفی کنیم:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IMyDisposableService, MyDisposableService>();
و سپس به نحو متداولی از آن در یک کنترلر استفاده کنیم:
namespace CoreIocSample02.Controllers
{
    public class HomeController : Controller
    {
        private readonly IMyDisposableService _myDisposableService;

        public HomeController(IMyDisposableService myDisposableService)
        {
            _myDisposableService = myDisposableService;
        }

        public IActionResult Index()
        {
            _myDisposableService.Run();
            return View();
        }
در اینجا منظور از نحو‌ه‌ی متداول، همان تزریق در سازنده‌ی کلاس و درخواست وهله‌ای از این سرویس از IoC Container است؛ بجای ایجاد مستقیم آن.
در ادامه با اجرای برنامه، اگر به لاگ‌های آن دقت کنیم، این خروجی قابل مشاهده خواهد بود:
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
      Route matched with {action = "Index", controller = "Home"}. Executing action CoreIocSample02.Controllers.HomeController.Index (CoreIocSample02)
info: CoreIocServices.MyDisposableService[0]
      + MyDisposableService was created
.
.
.  
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'CoreIocSample02.Controllers.HomeController.Index (CoreIocSample02)'
info: CoreIocServices.MyDisposableService[0]
      - MyDisposableService was disposed!
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 1316.4719ms 200 text/html; charset=utf-8
در ابتدای اجرای درخواست، پیام MyDisposableService was created ظاهر شده‌است (پیام صادر شده‌ی از سازنده‌ی سرویس) و جائیکه پیام Executed endpoint یا پایان درخواست جاری لاگ شده، بلافاصله پیام MyDisposableService was disposed نیز مشاهده می‌شود که از متد Dispose سرویس درخواستی صادر شده‌است.
بنابراین IoC Container، به صورت خودکار، کار Dispose این سرویس IDisposable را نیز انجام داده‌است.


Dispose خودکار وهله‌هایی که توسط IoC Container ایجاد نشده‌اند

اگر ایجاد اشیاء از نوع IDisposable را خودتان و خارج از دید IoC Container توکار ASP.NET Core انجام می‌دهید، از مزیت پاکسازی خودکار منابع توسط آن‌ها در پایان درخواست محروم خواهید شد، اما ... برای رفع این مشکل نیز متد context.Response.RegisterForDispose پیش بینی شده‌است. اگر شیءای از نوع IDisposable را توسط این متد به ASP.NET Core معرفی کنید، در پایان درخواست به صورت خودکار Dispose خواهد شد.
یک مثال: فرض کنید یک StreamWriter را داخل یک میان‌افزار ایجاد کرده‌اید، اما آن‌را Dispose نکرده‌اید:
namespace CoreIocSample02
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.Use(async (context, next) =>
            {
                var writer = File.CreateText(Path.GetTempFileName());
                context.Response.RegisterForDispose(writer);
                context.Items["filewriter"] = writer;
                await writer.WriteLineAsync("some important information");
                await writer.FlushAsync();
                await next();
            });
در این مثال، شیء writer به context.Items انتساب داده شده‌است تا در سایر قسمت‌های pipeline جاری نیز قابل دسترسی باشد. یعنی از آن می‌توان داخل یک اکشن متد نیز استفاده کرد. نکته‌ی مهم آن، معرفی این شیء به متد context.Response.RegisterForDispose است که سبب خواهد شد پس از پایان کار درخواست، به صورت خودکار writer را Dispose کند.
اکنون در ادامه، در اکشن متد WriteLog یک کنترلر دلخواه، کار ثبت وقایع با دریافت این writer از HttpContext.Items قابل انجام است؛ چون هنوز طول عمر درخواست جاری پایان نیافته و شیء writer به صورت خودکار Dispose نشده‌است:
namespace CoreIocSample02.Controllers
{
    public class HomeController : Controller
    {
        public async Task<IActionResult> WriteLog()
        {
            var writer = HttpContext.Items["filewriter"] as StreamWriter;
            if (writer != null)
            {
                await writer.WriteLineAsync("more important information");
                await writer.FlushAsync();
            }
            return View();
        }
 
روش صحیح Dispose اشیایی با طول عمر Scoped، در خارج از طول عمر یک درخواست ASP.NET Core

زمانیکه به صورت متداولی از سیستم تزریق وابستگی‌های ASP.NET Core استفاده می‌کنیم، به ازای هر درخواست HTTP رسیده، یک Scope از نوع IServiceScopeFactory ایجاد می‌شود و با پایان درخواست، این Scope نیز Dispose خواهد شد. به این ترتیب هر سرویس ایجاد شده‌ی درون این Scope نیز Dispose می‌شود؛ کاری شبیه به عملیات زیر:
using(var scope = serviceProvider.CreateScope())
{
   var provider = scope.ServiceProvider;
   var resolvedService = provider.GetRequiredService(someType);
   // Use resolvedService...
}
در این بین سرویس‌های Singleton به هیچ Scope ای منتسب نمی‌شوند و طول عمر آن‌ها توسط root container مدیریت می‌شود و زمانیکه این ServiceProvider یا root container به پایان کار خودش برسد، با dispose شدن آن، سرویس‌های Singleton آن نیز dispose خواهند شد.

مشکل! اگر از سرویس فرضی IOperationScoped با طول عمر Scoped در متدهای مختلف کلاس آغازین برنامه استفاده کنیم (مانند DbContext برنامه)، طول عمری را که دریافت خواهیم کرد singleton خواهد بود و نه Scoped؛ چون درون یک scopeFactory.CreateScope ایجاد شده‌ی به صورت خودکار توسط یک درخواست قرار نداریم. بنابراین هر درخواست وهله‌ای از سرویس IOperationScoped با طول عمر Scoped، تنها همان وهله‌ی ابتدایی آن‌را باز می‌گرداند و singleton رفتار می‌کند؛ چون scope ایی ایجاد و تخریب نشده‌است.
در یک چنین مواردی، برای اطمینان حاصل کردن از dispose شدن سرویس در پایان کار، نیاز است مراحل ایجاد scope و dispose آن‌را به صورت دستی به نحو ذیل مدیریت کنیم:
public void Configure(IApplicationBuilder app, 
                      ILoggerFactory loggerFactory, 
                      IServiceScopeFactory scopeFactory) 
{
   using (var scope = scopeFactory.CreateScope()) 
   { 
     var initializer = scope.ServiceProvider.GetService<IOperationScoped>(); 
     initializer.SeedAsync().Wait(); 
   } 
}


Dispose کردن سرویس‌های IDisposable در برنامه‌های Console

اگر همین سرویس IMyDisposableService را در مثال برنامه‌ی کنسول قسمت اول استفاده کنیم:
var myDisposableService = serviceProvider.GetService<IMyDisposableService>();
myDisposableService.Run();
در پایان کار برنامه، شاهد پیام MyDisposableService was disposed نخواهیم بود. به همین جهت در اینجا نیز می‌توانیم شبیه به کاری که در ASP.NET Core در پشت صحنه رخ می‌دهد، عمل کنیم:
در برنامه‌ی کنسول، کار ایجاد serviceProvider را خودمان انجام دادیم:
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
var serviceProvider = serviceCollection.BuildServiceProvider();
متد BuildServiceProvider خروجی از نوع کلاس ServiceProvider را دارد؛ با این امضاء:
namespace Microsoft.Extensions.DependencyInjection
{
    public sealed class ServiceProvider : IServiceProvider, IDisposable, IServiceProviderEngineCallback
    {
        public void Dispose();
        public object GetService(Type serviceType);
    }
}
همانطور که مشاهده کنید، کلاس ServiceProvider نیز اینترفیس IDisposable را پیاده سازی می‌کند. بنابراین برای آزاد سازی صحیح منابع وابسته‌ی به آن، باید متد Dispose آن‌را نیز فراخوانی کرد:
namespace CoreIocSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            var serviceCollection = new ServiceCollection();
            ConfigureServices(serviceCollection);
            using (var serviceProvider = serviceCollection.BuildServiceProvider())
            {
                var myDisposableService = serviceProvider.GetService<IMyDisposableService>();
                myDisposableService.Run();

                var testService = serviceProvider.GetService<ITestService>();
                testService.Run();
            }
        }
در اینجا serviceProvider را داخل یک using statement قرار داده‌ایم. اینبار اگر برنامه را اجرا کنیم، پس از پایان کار برنامه، پیام MyDisposableService was disposed نیز ظاهر می‌شود. ServiceProvider ایجاد شده یا همان root container، در زمان Dispose، تمام اشیایی را هم که توسط آن مدیریت شده‌اند و نیاز به Dispose دارند، Dispose می‌کند.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: CoreDependencyInjectionSamples-02.zip