رفتار 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); } } }
اگر این سرویس را به یک برنامهی 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(); }
در ادامه با اجرای برنامه، اگر به لاگهای آن دقت کنیم، این خروجی قابل مشاهده خواهد بود:
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
بنابراین 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(); });
اکنون در ادامه، در اکشن متد 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(); }
زمانیکه به صورت متداولی از سیستم تزریق وابستگیهای 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... }
مشکل! اگر از سرویس فرضی 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();
در برنامهی کنسول، کار ایجاد serviceProvider را خودمان انجام دادیم:
var serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); var serviceProvider = serviceCollection.BuildServiceProvider();
namespace Microsoft.Extensions.DependencyInjection { public sealed class ServiceProvider : IServiceProvider, IDisposable, IServiceProviderEngineCallback { public void Dispose(); public object GetService(Type serviceType); } }
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(); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: CoreDependencyInjectionSamples-02.zip