تزریق وابستگی‌ها در ASP.NET Core - بخش 4 - طول حیات سرویس ها یا Service Lifetime
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: شش دقیقه

در قسمت‌های قبلی این سری، به ترتیب ابتدا در مورد مبحث تزریق وابستگی‌ها صبحت کردیم، بعد اولین سرویس‌مان را در ASP.NET Core ثبت و واکشی کردیم. در بخش سوم، تنظیمات را درون سامانه، ثبت و استفاده کردیم و حالا در این بخش می‌خواهیم به مبحث طول حیات سرویس‌ها بپردازیم.
همانطور که گفتیم، وظیفه‌ی DI Container، ایجاد یک نمونه از سرویس درخواست شده، تزریق آن به کلاس درخواست دهنده و در انتها از بین بردن یا Dispose شیء ایجاد شده از سرویس ثبت شده‌است. بنابراین ما باید در هنگام ثبت سرویس، بر اساس تحلیل و نیاز برنامه‌ی خودمان، طول عمر سرویس/Service Life Time را مشخص کنیم.

بصورت کلی در Microsoft Dependency Injection Container و اکثر DI Container‌های دیگر، 3 نوع کلی چرخه‌ی حیات وجود دارند که به ترتیب پایداری و طول عمر شیء ایجاد شده، در زیر آورده شده‌اند:
  •  Singleton
  •  Scoped
  •  Transient

Singleton (یگانه)

فقط و فقط یک شیء از سرویس ثبت شده با این طول عمر، در اولین درخواست ایجاد می‌شود و سپس در کل طول حیات برنامه، از همین شیء ایجاد شده، استفاده می‌گردد.  به همین دلیل به آن «یگانه» یا Singleton می‌گویند. هر زمانیکه این سرویس در خواست داده می‌شود، DI Container، همان یک شیء را در اختیار درخواست دهنده قرار می‌دهد و این شیء، هیچگاه از بین نمی‌رود.  به بیان دیگر، DI Container هیچگاه این شیء را از بین نمی‌برد. شیء ساخته شده از سرویس ثبت شده‌ی با حالت Singleton، بین تمامی استفاده کنندگان، به صورت اشتراکی استفاده می‌شود. این طول عمر تقریبا مشابه‌ی اشیاء ساخته شده توسط Singleton Pattern عمل می‌کند.
با توجه به مطالب گفته شده، ویژگی‌های سرویس‌های Singleton به شرح زیر هستند:
  •   در اولین درخواست به سرویس، یک نمونه از آن ساخته می‌شود و تا پایان برنامه در حافظه نگه داشته می‌شود.
  •   در سایر درخواست‌ها، همان یک نمونه‌ی ساخته شده‌ی از سرویس، ارائه داده می‌شود. 
  •   به علت موجود بودن در حافظه، معمولا دسترسی به آن‌ها و عملکرد آن‌ها سریعتر است.
  •   بار کاری بر روی Garbage Collector فریمورک را کاهش می‌دهند.

بنابراین در هنگام تعریف کردن یک سرویس به صورت Singleton باید نکات زیر را مد نظر قرار بدهید:
  • باید سرویس مورد نظر Thread Safe باشد .
  •  نباید استفاده کننده‌ی از این سرویس، امکان تغییر State آن را داشته باشد.
  •  اگر ساخت شیء‌ای از یک سرویس، هزینه‌ی زیادی را داشته باشد ، احتمالا Singleton کردن آن می‌تواند ایده‌ی خوبی باشد.
  •  شیء ساخته شده‌ی از این سرویس، تا زمان اجرای برنامه، بخشی از حافظه‌ی برنامه را اشغال می‌کند. پس باید حجم اشغالی در حافظه را نیز مد نظر قرار داد.
  •  تعداد دفعات استفاده را در برابر مصرف حافظه در نظر بگیرید.
معمولا سرویس‌هایی مثل تنظیمات برنامه، از این نوع تعریف می‌شوند.

برای ثبت یک سرویس به صورت Singleton می‌توانیم از متدهای توسعه‌ای با نام ()AddSingleton، با سربارهای مختلف بر روی IServiceCollection استفاده کنیم. علاوه بر این، در هنگام استفاده از Option Pattern، متد Configure، خودش سرویس مورد نظر را به صورت Singleton ثبت می‌کند.

خب، به روش زیر سرویس GuidProvider را بعنوان یک Singleton  تعریف می‌کنیم:
services.AddSingleton(services => new GuidProvider());
 اکنون این سرویس را درون اکشن Index  و کنترلر HomeController تزریق می‌کنیم:
        public HomeController(ILogger<HomeController> logger,
            IMessageServiceA messageService,
            LiteDbConfig liteDbConfig,
            GuidProvider guidHelper)
        {
            _logger = logger;
            _messageService = messageService;
            _messageService = new MessageServiceAA();
            _guidHelper = guidHelper;
        }

حالا اگر برنامه را اجرا کنیم، می‌بینید که با تازه سازی صفحه‌ی Home/Index ، همچنان Id، برابر با یک رشته‌ی یکسان است. حتی اگر تب دیگری را در مرورگر باز کنیم و دوباره به این صفحه برویم، می‌بینیم که Id برابر همان رشته‌ی قبلی است و دلیل این موضوع، ثبت سرویس Guid Service به صورت Singleton است.


Scoped ( محدود شده )

به ازای هر درخواست (در اینجا معمولا درخواست‌های Http مد نظر است) یک نمونه از این سرویس ساخته می‌شود و در طول حیات این درخواست، DI Container به هر کلاسی که به این سرویس نیاز دارد، همان یک نمونه را برگشت می‌دهد و این نمونه در کل طول اجرای این درخواست، بین تمامی سرویس گیرندگان، یکسان است. هر زمانی، درخواست به پایان برسد، نمونه‌ی ساخته شده از سرویس، Disposed می‌گردد و GC می‌تواند آن را از بین ببرد.

معمولا سرویس‌های اتصال به پایگاه داده‌ها و کار بر روی آنها که شامل خواندن، نوشتن، ویرایش، حذف می‌شوند را با طول حیات Scoped ، درون DI Container ثبت می‌کنند . EF Core به صورت پیش فرض ، Db Context را به صورت Scoped ثبت می‌کند.

سرویس‌های Scoped در محدوده‌ی درخواست، مانند  Singleton عمل می‌کنند و شیء ساخته شده و وضعیت آن در بین تمامی سرویس‌هایی  که به آن نیاز دارند، مشترک است. بنابراین باید به این نکته در هنگام تعریف سرویس به صورت Scoped ، توجه داشته باشید.

تمام Middleware ‌های ASP.NET Core هم فقط همان نمونه‌ی ایجاد شده از سرویس Scoped را در طی اجرای یک درخواست خاص، می‌گیرند.

هر سرویسی که به سرویس‌های Scoped نیاز دارد، یا باید به صورت Transient و یا باید به صورت Scoped ثبت شود، تا مانع از این شویم که شیء ساخته شده، فراتر از طول حیات موردنظرمان، در حافظه بماند و از آن استفاده شود .

برای ثبت یک سرویس به صورت Scoped می‌توانیم از متدهای توسعه‌ای با نام AddScoped() با سربارهای مختلف بر روی IServiceCollection استفاده کنیم. در اینجا از نسخه‌ای که دو پارامتر جنریک را می‌گیرد، برای ثبت یک سرویس به صورت Scoped استفاده می‌کنیم:

services.AddScoped<IMessageServiceB, MessageServiceBA>();

می توانیم سرویس GuidProvider را  به جای Signleton ، به صورت Scoped ثبت کنیم: 

services.AddScoped(services => new GuidProvider());
حال اگر برنامه را اجرا کنیم، می بینید که این بار با تازه سازی صفحه‌ی Home/Index، مقدار  Id برابر با یک رشته‌ی جدید است.  

 

Transient (گذرا)

به ازای هر درخواست دهنده‌ی جدید، یک نمونه‌ی جدید از سرویس، توسط DI Container ساخته می‌شود و در اختیار آن قرار می‌گیرد.

سرویس‌هایی را به این صورت ثبت کنید که:

  •   نیاز به Thread Safe بودن داشته باشند.
  • نمی توانید طول عمر سرویس را حدس بزنید.

سرویس‌های Transient ، کارآیی پائین‌تری دارند و سربار عملکردی زیادی بر روی Garbage Collector می گذارند؛ ولی به علت اینکه به ازای هر واکشی، یک نمونه‌ی جدید از آن‌ها ساخته می‌شود و State بین این اشیاء به اشتراک گذاشته نمی‌شود، امنیت بیشتری دارند و درک و خطایابی آنها ساده‌تر است.

برای ثبت سرویس‌های Transient از متد توسعه‌ای AddTransient() استفاده می‌کنیم. سربارهای این متد مانند سربارهای متدهای AddSingleton() و AddScoped() است:

services.AddTransient<IMessageServiceC, MessageServiceCA>();

 

وابستگی‌های محصور شده

یک سرویس نباید وابسته‌ی به سرویسی باشد که طول حیاتی کمتر از طول حیات خودش را دارد.

برای مثال اگر درون سرویسی با طول حیات Singleton، از یک سرویس با طول حیات Transient استفاده کنیم، اینکار باعث می‌شود که یک نمونه از سرویس Transient در طول حیات برنامه، همیشه درون حافظه بماند و این ممکن است باعث خطاهای عجیبی در هنگام اجرا شود که معمولا خطایابی و رفع آن‌ها مشکل است.


اثرات جانبی وابستگی‌های محصور شده:

  • به اشتراک گذاری تصادفی وضعیت یک شیء بین Thread ‌ها درون سرویس‌هایی که Thread Safe نیستند.
  • اشیاء، بیش از زمان پیش بینی شده‌ی برایشان، درون حافظه باقی می‌مانند.


سرویس‌های Transient می‌توانند به سرویس‌هایی با طول حیات زیر وابستگی داشته باشند:

  •   Transient
  •   Scoped
  •   Singleton

 

سرویس‌های Scoped می‌توانند به سرویس‌هایی با طول حیات زیر وابستگی داشته باشند:

  • Transient
  •   Scoped


سرویس‌های Singleton می‌توانند به سرویس هایی با طول حیات زیر وابستگی داشته باشند:

Singleton  


می‌توانید از جدول زیر به عنوان راهنمای خلاصه شده‌ی برای استفاده‌ی امن از سرویس‌ها درون یکدیگر بهره ببرید:


Scope Validation 

این قابلیت که به صورت پیش فرض در حالت توسعه‌ی برنامه‌های ASP.NET Core فعال است، در زمان شروع برنامه و در Startup ، بررسی می‌کند که سرویس‌ها، وابستگی به سرویس‌هایی با طول حیاتی مناسب، داشته باشند.

  • #
    ‫۴ سال و ۳ ماه قبل، پنجشنبه ۱۵ خرداد ۱۳۹۹، ساعت ۰۳:۰۰

    در متن آمده که  EF Core  به صورت پیش فرض ،  Db Context  را به صورت  Scoped  ثبت می‌کند. و نحوه تزریق از طریق سازنده کلاس‌ها است. از طرفی اگر درست متوجه شده باشم در مستندات ماکروسافت هشدار داده شده که این مدل طول حیات را نباید از طریق سازنده تزریق کرد.


    When using a scoped service in a middleware, inject the service into the Invoke or InvokeAsync method. Don't inject via constructor injection because it forces the service to behave like a singleton. For more information, see Write custom ASP.NET Core middleware. 


    همچنین شما گفتید که "سرویس‌های Scoped  در محدوده‌ی درخواست، مانند  Singleton عمل می‌کنند و شیء ساخته شده و وضعیت آن در  بین تمامی سرویس‌هایی  که به آن نیاز دارند، مشترک است. بنابراین باید به این نکته در هنگام تعریف سرویس به صورت  Scoped ، توجه داشته باشید."


    در پروژه‌ای که روی آن کار میکنم (ASP.NET Core Razor Pages) یک کلاس validation هست که میخوام به عنوان یک سرویس به کلاس هر صفحه تزریق بشه. اگر امکانش هست توضیح بدید که بر اساس مواردی که در بالا آوردم و هدفی که دارم آیا بین حالات singleton و scoped فرقی وجود دارد؟

    ممنون.

    • #
      ‫۴ سال و ۳ ماه قبل، پنجشنبه ۱۵ خرداد ۱۳۹۹، ساعت ۰۶:۲۷
      آقای نصیری یک متن در این مورد دارند که لینکش رو ارسال کردند .
      در مورد میان افزار‌ها ، قسمت 7 مقاله هست که به خاطر مشغله‌ی کاری هنوز اون رو ننوشتم ... اگر وقت بود در مورد واکشی سرویس‌ها و مکانیزم‌های واکشی سرویس‌ها هم در ادامه توضیح خواهم داد .

      میان افزار / Middle ware‌ها :
      این کلاس‌ها فقط و فقط در هنگام تعریف در متد Configure ساخته می‌شوند و  شی ایجاد شده از هر سرویسی که درون سازنده‌ی آن‌ها ثبت بشه ، تا پایان طول حیات برنامه ، در حافظه نگه داشته می‌شود و اصطلاحا Capture می‌شود .
      مثلا اگر میان افزاری برای لاگ کردن آخرین فعالیت کاربر بنویسیم و UserRepository را در سازنده تعریف کنیم ، یک نمونه از این Repository تا در طول حیات برنامه در حافظه نگه داشته می‌شود که معمولا باعث  اشغال حافظه ، باز نگه داشتن Connection به پایگاه داده و ایجاد خطا می‌شود . برای جلوگیری از این مشکل ، در میان افزارها ، ما سرویس‌های Transient و Scoped را در خود متد‌های InvokeAsync و یا Invoke ثبت می‌کنیم تا با هر درخواست جدید یک نمونه‌ی جدید از آن‌ها ساخته شود و با پایان درخواست نمونه‌ی ساخته شده به درستی از بین برود .

      در مورد Validation :
      به صورت معمول ، Validation در هر صفحه به ازای هر درخواست ، از ابتدا انجام می‌شود و بنابراین روش منطقی ثبت و واکشی سرویس‌های Validation به صورت Scoped هست تا خطر به اشتراک گذاشتن وضعیت شی وجود نداشته باشد . توصیه‌ی من ثبت این سرویس به صورت Scoped و یا حتی برای امنیت بیشتر ، ثبت آن به صورت Transient هست تا مطمئن شوید که با هر بار فراخوانی این سرویس در طول یک درخواست ، یک نمونه‌ی جدید ساخته و استفاده می‌شود .

      "سرویس‌های Scoped  در محدوده‌ی درخواست، مانند  Singleton عمل می‌کنند و شیء ساخته شده و وضعیت آن در  بین تمامی سرویس‌هایی  که به آن نیاز دارند، مشترک است. بنابراین باید به این نکته در هنگام تعریف سرویس به صورت  Scoped ، توجه داشته باشید."  »
      یک مثال می‌زنیم فرض کنید IUserRepository را به عنوان یک سرویس Scoped ثبت کرده ایم ، و دو سرویس IUserAccountManager و IUserFinancialManager به این سرویس وابسته هستند و این سرویس درون سازنده‌ی دو سرویس Manager ثبت شده هست . حالا این دو سرویس خودشان درون UserController ثبت شده اند .

      public class UserAccountManager  : IUserAccountManager 
      {
           private I,serRepository _userRepository ; 
           // تزریق درون سازنده
      }
      
      public class UserFinancialManager  : IUserFinancialManager  
      {
           private IUserRepository _userRepository ; 
           // تزریق درون سازنده
      }
      
      
      public class UserController 
      {
           IUserFinancialManager  _userFinancialManager ;
           IUserAccountManager  _userAccountManager;
      
           // تزریق درون سازنده
      }

      در این حالت ، DI Container به ازای هر درخواست به این کنترلر ، یک نمونه از userRepository می‌سازد و این نمونه را در اختیار UserAccountManager و UserFinancialManager می‌گذارد و در طول درخواست ، هر تغییر وضعیتی درون userRepository بین دو سرویس Manager ، مشترک هست ... این معنای « "سرویس‌های Scoped  در محدوده‌ی درخواست، مانند  Singleton عمل می‌کنند و شیء ساخته شده و وضعیت آن در  بین تمامی سرویس‌هایی  که به آن نیاز دارند، مشترک است.  » می‌باشد ... به عبارت ساده‌تر ،در این مثال هر دو سرویس Manager به یک نمونه از DbContext درون UserRepository دسترسی دارند .

      حالا فرض کنید در اینجا IUserRepository را به صورت Transient ثبت کرده باشیم ، در این حالت به ازای هر درخواست به کنترلر مورد بحث ، DI Container برای هر سرویس Manager ، یک نمونه‌ی اختصاصی از IUserRepository می‌سازد و در اختیار آن‌ها قرار می‌دهد و هر سرویس یک نمونه‌ی منحصر به فرد از IUserRepository دارد . به عبارت ساده‌تر ، در این مثال هر سرویس Manager به یک نمونه‌ی اختصاصی از DbContet دسترسی دارد .

      برای تست این موضوع می‌توانید از تکه کد زیر استفاده کنید :

      using System;
      
      namespace AspNetCoreDependencyInjection.Services
      {
          public interface IUserRepository
          {
              public string GID { get; }
          }
      
          public class UserRepository : IUserRepository
          {
              public string GID { get; private set; }
      
              public UserRepository()
              {
                  GID = Guid.NewGuid().GetHashCode().ToString("x");
              }
          }
      
          //---------------------------------------------
      
          public interface IUserAccountManager
          {
              public string DisplayGuid();
          }
      
          public class UserAccountManager : IUserAccountManager
          {
              private IUserRepository _userRepository;
      
              public UserAccountManager(IUserRepository userRepository)
              {
                  _userRepository = userRepository;
              }
      
              public string DisplayGuid()
              {
                  return "UserFinancialManager Guid : " + _userRepository.GID;
              }
          }
      
          //-------------------------------------------------
      
          public interface IUserFinancialManager
          {
              public string DisplayGuid();
          }
      
          public class UserFinancialManager : IUserFinancialManager
          {
              private IUserRepository _userRepository;
      
              public UserFinancialManager(IUserRepository userRepository)
              {
                  _userRepository = userRepository;
              }
      
              public string DisplayGuid()
              {
                  return "UserFinancialManager Guid : " + _userRepository.GID;
              }
          }
      }

      حالا در کنترلر

      public class UserController : Controller
          {
             
              private readonly IUserFinancialManager _userFinancialManager;
      
              private readonly IUserAccountManager _userAccountManager;
      
              public UserController(IUserFinancialManager userFinancialManager,
                  IUserAccountManager userAccountManager)
              {
                  _userFinancialManager = userFinancialManager;
                  _userAccountManager = userAccountManager;
              }
      
              public IActionResult Index()
              {
                  ViewBag.FinancialManager = _userFinancialManager.DisplayGuid();
                  ViewBag.AccountManager = _userAccountManager.DisplayGuid();
                  return View();
              }
          }

      و نمای ایندکس

      @{
      ViewData["Title"] = "User";
      }
      
      <div class="text-center">
      <div class="row">
      <div class="col-12">
      <p class="alert alert-info text-left">
      <b>User Finanacial Manager / UserRepository Guid  : </b><span>@ViewBag.FinancialManager </span> <br />
      <b>User Account Manager / UserRepository Guid  : </b><span>@ViewBag.AccountManager</span> <br />
      </p>
      </div>
      </div>
      </div>

      برای تست یکبار سرویس IUserRepository را به صورت Scoped و بار دیگر به صورت Transient ثبت کنید و با اجرا برنامه Guid‌های ایجاد شده را چک کنید .

       services.AddTransient<IUserRepository, UserRepository>();
       services.AddScoped<IUserAccountManager, UserAccountManager>();
      services.AddScoped<IUserFinancialManager, UserFinancialManager>();
      
      
      // اجرای دوم 
      // services.AddScoped<IUserRepository, UserRepository>();
       //services.AddScoped<IUserAccountManager, UserAccountManager>();
      //services.AddScoped<IUserFinancialManager, UserFinancialManager>();





  • #
    ‫۲ سال و ۴ ماه قبل، سه‌شنبه ۲۳ فروردین ۱۴۰۱، ساعت ۱۷:۵۴
    سرویس‌های  Scoped   در محدوده‌ی درخواست، مانند    Singleton   عمل می‌کنند و شیء ساخته شده و وضعیت آن در  بین تمامی سرویس‌هایی  که به آن نیاز دارند، مشترک است. بنابراین باید به این نکته در هنگام تعریف سرویس به صورت  Scoped   ، توجه داشته باشید. 

    سرویس هایی که به صورت scoped رجیستر میشوند در حالتی مثل استفاده در parallel یا concurrent آیا thread-safe هستند یا خیر؟
    • #
      ‫۲ سال و ۴ ماه قبل، سه‌شنبه ۲۳ فروردین ۱۴۰۱، ساعت ۱۸:۴۴
      خیر.
      • #
        ‫۲ سال و ۴ ماه قبل، سه‌شنبه ۲۳ فروردین ۱۴۰۱، ساعت ۲۰:۵۹
        در داکیومنت‌ها به این نکته اشاره شده که سرویس‌های scoped، نیازی به طراحی به صورت thread-safe نیستند و امن هستند. 
        آیا این به معنی این هست که ذاتا در درون هر وب ریکوئست و در یک ترد، امن هستند ولی به منزله thread-safe بودنشون در برنامه نویس multithread نیست؟
        • #
          ‫۲ سال و ۴ ماه قبل، سه‌شنبه ۲۳ فروردین ۱۴۰۱، ساعت ۲۲:۴۹
          - هر درخواست ASP.NET Core، توسط یک ترد مدیریت می‌شود. بنابراین، هرچند کل برنامه‌ی وب، چندریسمانی است، اما به معنای به اشتراک گذاری اطلاعات آن درخواست خاص، با تردها و درخواست‌های دیگر نیست.
          - Scoped بودن یک سرویس، فقط به معنای یکبار نمونه سازی آن در طول یک درخواست هست و هیچ معنای دیگری ندارد و هیچ ضمانت خاصی به همراه آن نیست.
          - آیا می‌توان در طول یک درخواست، اطلاعات این سرویس Scoped را توسط سرویس‌های دیگر تغییر داد؟ بله. نمونه‌اش پیاده سازی الگوی واحد کار با Scoped تعریف کردن DbContext برنامه است. سرویس‌های مختلف، بر روی اطلاعات این یک نمونه از سرویس در دسترس، می‌توانند تاثیر گذار باشند و در آخر کار فقط یک تراکنش Commit شود.
          - آیا اگر درون یک درخواست، آن یک نمونه از سرویس Scoped را به صورت چندریسمانی استفاده کنیم، thread-safe است؟ خیر. اطلاعات بیشتر.
  • #
    ‫۲ سال و ۴ ماه قبل، دوشنبه ۵ اردیبهشت ۱۴۰۱، ساعت ۱۹:۵۴
    بنظرم این بخش متن مشکل داره:

    سرویس‌های Scoped  می‌توانند به سرویس‌هایی با طول حیات زیر وابستگی داشته باشند:
      Transient
      Scoped

    کمی بالاتر گفته شده « یک سرویس نباید وابسته‌ی به سرویسی باشد که طول حیاتی کمتر از طول حیات خودش را دارد» در صورتیکه در بالا گفته شده شیء با طول عمر Scoped میتونه وابسته به شیء با طول عمر Transient باشد.
    • #
      ‫۲ سال و ۴ ماه قبل، دوشنبه ۵ اردیبهشت ۱۴۰۱، ساعت ۲۲:۱۷
      زمانیکه یک سرویس با طول عمر Scoped ایجاد می‌شود، هر سرویس Transient ای که به همراه آن و در طول عمر آن درخواست شود، همان طول عمر Scoped را پیدا می‌کند و نه کمتر از آن. Transient به معنای طول عمر کوتاه نیست. به معنای هربار که به آن نیاز است و تزریق شده، مجددا نمونه سازی شود، هست. برای درک بهتر مدیریت طول عمر آن، «روش صحیح Dispose اشیایی با طول عمر Scoped، در خارج از طول عمر یک درخواست ASP.NET Core» را مطالعه کنید.