در قلب معماری ASP.NET Core، سیستم تزریق وابستگی (Dependency Injection - DI) قرار دارد که شالوده‌ای برای ساخت برنامه‌های کاربردی انعطاف‌پذیر، قابل آزمایش و با قابلیت نگهداری بالا فراهم می‌کند. یکی از جنبه‌های حیاتی پیکربندی DI، تعریف طول عمر (Lifetime) سرویس‌ها است. طول عمر سرویس تعیین می‌کند که یک نمونه از سرویس چه مدت در طول چرخه حیات برنامه باقی بماند و چه زمانی نمونه جدیدی ایجاد شود. در فایل Program.cs (یا کلاس Startup در نسخه‌های قدیمی‌تر)، متدهای توسعه‌ای مانند AddSingleton()‎، AddScoped()‎ و AddTransient()‎ نقش کلیدی در تعیین این طول عمر ایفا می‌کنند. درک تفاوت‌های ظریف بین این سه نوع طول عمر برای توسعه‌دهندگان ASP.NET Core ضروری است تا از مدیریت صحیح منابع، عملکرد بهینه و رفتار قابل پیش‌بینی برنامه خود اطمینان حاصل کنند.
کینگتو - آموزش برنامه نویسی تخصصصی - دات نت - سی شارپ - بانک اطلاعاتی و امنیت

طول عمر سرویس‌ها در ASP.NET Core: راهنمای جامع Singleton، Scoped و Transient

29 بازدید 0 نظر ۱۴۰۴/۰۲/۰۹

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

مبانی تزریق وابستگی و ثبت سرویس‌ها
پیش از پرداختن به جزئیات طول عمر سرویس‌ها، مروری مختصر بر مفهوم تزریق وابستگی و نحوه ثبت سرویس‌ها در ASP.NET Core ضروری است. تزریق وابستگی یک الگوی طراحی است که در آن وابستگی‌های یک شیء (سرویس‌ها) به جای اینکه در داخل خود شیء ایجاد شوند، از خارج به آن تزریق می‌شوند. این امر منجر به کاهش وابستگی بین اجزاء، افزایش قابلیت آزمایش و بهبود سازماندهی کد می‌شود.

در ASP.NET Core، کانتینر DI داخلی مسئول مدیریت ایجاد و طول عمر سرویس‌ها است. توسعه‌دهندگان با استفاده از متدهای توسعه‌ای ارائه شده توسط رابط IServiceCollection (که در شیء builder.Services در Program.cs قابل دسترسی است) سرویس‌های خود را در این کانتینر ثبت می‌کنند. هر ثبت سرویس شامل نوع سرویس (یک رابط یا کلاس پایه) و نوع پیاده‌سازی (کلاس concrete که رابط یا کلاس پایه را پیاده‌سازی می‌کند) و همچنین طول عمر مورد نظر است.

Singleton

Singleton: یک نمونه برای کل عمر برنامه
سرویس‌های ثبت شده با طول عمر Singleton، تنها یک نمونه از آن‌ها در طول کل چرخه حیات برنامه ایجاد می‌شود. این بدان معناست که هرگاه در هر نقطه‌ای از برنامه به این سرویس نیاز باشد، همان نمونه واحد ارائه خواهد شد.

مکانیسم عملکرد:

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

سناریوهای استفاده مناسب:

  • سرویس‌های بدون حالت (Stateless Services): سرویس‌هایی که هیچ داده‌ای را در طول درخواست‌ها ذخیره نمی‌کنند و رفتار آن‌ها به ورودی‌های ارائه شده بستگی دارد، کاندیدای خوبی برای Singleton بودن هستند. به عنوان مثال، یک سرویس برای تولید شناسه‌های یکتا یا یک سرویس برای انجام محاسبات ریاضی.
  • سرویس‌های پیکربندی: سرویس‌هایی که تنظیمات سراسری برنامه را مدیریت می‌کنند و در طول اجرای برنامه ثابت هستند، می‌توانند به صورت Singleton ثبت شوند.
  • سرویس‌های حافظه پنهان (Caching): پیاده‌سازی یک حافظه پنهان سراسری در برنامه می‌تواند به عنوان یک سرویس Singleton مدیریت شود.
  • سرویس‌های لاگینگ (Logging): یک سرویس لاگینگ که مسئول نوشتن رویدادها در یک فایل یا پایگاه داده است، معمولاً به صورت Singleton پیاده‌سازی می‌شود.


ملاحظات و موارد احتیاط:

  • اشتراک‌گذاری State: از آنجایی که تنها یک نمونه از سرویس Singleton وجود دارد، هر گونه حالت (state) که در داخل آن ذخیره شود، بین تمام درخواست‌ها و کاربران به اشتراک گذاشته خواهد شد. این می‌تواند منجر به مشکلات همزمانی و رفتار غیرمنتظره شود اگر سرویس به درستی برای مدیریت دسترسی همزمان طراحی نشده باشد.
  • مصرف حافظه: اگر یک سرویس Singleton منابع قابل توجهی را در حافظه نگه دارد، این منابع تا پایان عمر برنامه اشغال خواهند شد.

public interface ICounter
{
    int Increment();
    int GetCount();
}

public class Counter : ICounter
{
    private int _count = 0;

    public int Increment()
    {
        return ++_count;
    }

    public int GetCount()
    {
        return _count;
    }
}

در Program.cs:

builder.Services.AddSingleton();

در این مثال، هر بار که ICounter در هر جای برنامه تزریق شود، یک نمونه واحد از کلاس Counter دریافت خواهد شد و متد Increment() آن برای تمام درخواست‌ها یک شمارنده مشترک را افزایش می‌دهد.

در مثال ارائه شده که سرویس ICounter با طول عمر Singleton ثبت شده است:

builder.Services.AddSingleton<ICounter, Counter>();

تنها یک نمونه از کلاس Counter در کل طول عمر برنامه ایجاد می‌شود. این بدان معناست که تمام درخواست‌ها و کاربران مختلف (مانند کاربر شماره یک و کاربر شماره دو که جداگانه لاگین کرده‌اند) به همان نمونه واحد از شیء Counter دسترسی خواهند داشت.

بنابراین، اگر کاربر شماره یک متد Increment() را فراخوانی کند، مقدار _count در آن نمونه واحد افزایش می‌یابد. هنگامی که کاربر شماره دو (یا هر کاربر دیگری) متد GetCount() را فراخوانی کند، مقدار به‌روز شده را که توسط کاربر شماره یک تغییر داده شده است، مشاهده خواهد کرد.

به عبارت دیگر، وضعیت (_count) در سرویس Singleton بین تمام کاربران و تمام درخواست‌ها به اشتراک گذاشته می‌شود.

 

Scoped

Scoped: یک نمونه در هر محدوده (Request Scope)
سرویس‌های ثبت شده با طول عمر Scoped، یک نمونه جدید در هر محدوده ایجاد می‌کنند. در یک برنامه وب ASP.NET Core، یک محدوده معمولاً با هر درخواست HTTP ورودی مرتبط است. این بدان معناست که در طول پردازش یک درخواست HTTP، هر بار که یک سرویس Scoped درخواست شود، یک نمونه یکسان از آن سرویس ارائه خواهد شد. با پایان یافتن درخواست HTTP، نمونه سرویس Scoped نیز از بین می‌رود.

مکانیسم عملکرد:

کانتینر DI یک محدوده را در ابتدای هر درخواست HTTP ایجاد می‌کند. هنگامی که یک سرویس Scoped برای اولین بار در طول این محدوده درخواست می‌شود، یک نمونه از آن ایجاد شده و در داخل محدوده ذخیره می‌شود. تمام درخواست‌های بعدی برای همان سرویس در طول همان محدوده، به همان نمونه ذخیره شده ارجاع داده می‌شوند. پس از اتمام پردازش درخواست و از بین رفتن محدوده، نمونه سرویس Scoped نیز برای جمع آوری زباله (Garbage Collection) واجد شرایط می‌شود.

سناریوهای استفاده مناسب:

  • سرویس‌های مرتبط با درخواست: سرویس‌هایی که داده‌ها یا عملیات خاص یک درخواست HTTP را مدیریت می‌کنند، اغلب به صورت Scoped ثبت می‌شوند. به عنوان مثال، یک سرویس برای دسترسی به داده‌های کاربر احراز هویت شده در طول یک درخواست.
  • Contextهای پایگاه داده (Database Contexts): در چارچوب‌های ORM مانند Entity Framework Core، DbContext معمولاً به صورت Scoped ثبت می‌شود تا اطمینان حاصل شود که تمام عملیات پایگاه داده در طول یک درخواست از یک اتصال واحد استفاده می‌کنند و تغییرات می‌توانند به صورت یک واحد تراکنشی مدیریت شوند.
  • سرویس‌های مدیریت وضعیت در سطح درخواست: سرویس‌هایی که وضعیت مربوط به یک درخواست خاص را نگهداری می‌کنند (مانند اطلاعات یک فرم در حال پردازش)، می‌توانند به صورت Scoped پیاده‌سازی شوند.


ملاحظات و موارد احتیاط:

  • اشتراک‌گذاری در سطح درخواست: سرویس‌های Scoped در طول یک درخواست HTTP به اشتراک گذاشته می‌شوند، اما نمونه‌های مختلفی برای درخواست‌های مختلف وجود خواهد داشت. این امر از مشکلات اشتراک‌گذاری حالت بین کاربران مختلف جلوگیری می‌کند.
  • وابستگی به Singleton: اگر یک سرویس Scoped به یک سرویس Singleton وابسته باشد، در طول هر درخواست HTTP از همان نمونه Singleton استفاده خواهد کرد.

public interface IRequestIdentifier
{
    string GetRequestId();
}

public class RequestIdentifier : IRequestIdentifier
{
    private readonly string _requestId;

    public RequestIdentifier()
    {
        _requestId = Guid.NewGuid().ToString();
    }

    public string GetRequestId()
    {
        return _requestId;
    }
}

در Program.cs:

builder.Services.AddScoped();

 
در این مثال، هر درخواست HTTP یک نمونه جدید از RequestIdentifier دریافت می‌کند و _requestId برای آن درخواست یکتا خواهد بود.
 
Transient

Transient: یک نمونه جدید در هر بار درخواست
سرویس‌های ثبت شده با طول عمر Transient، هر بار که درخواست شوند، یک نمونه جدید ایجاد می‌کنند. این بدان معناست که اگر یک سرویس Transient چندین بار در طول یک درخواست HTTP (یا در بخش‌های مختلف کد) تزریق شود، هر بار یک نمونه متفاوت از آن سرویس دریافت خواهد شد.

مکانیسم عملکرد:

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

سناریوهای استفاده مناسب:

  • سرویس‌های سبک و بدون حالت: سرویس‌هایی که عملیات کوتاه‌مدت و بدون نگهداری حالت انجام می‌دهند، کاندیدای خوبی برای Transient بودن هستند.
  • کارخانه‌ها (Factories): پیاده‌سازی الگوهای طراحی Factory که نیاز به ایجاد نمونه‌های جدید در هر بار درخواست دارند، معمولاً با استفاده از سرویس‌های Transient انجام می‌شود.
  • سرویس‌هایی با وابستگی‌های Scoped: در برخی موارد، یک سرویس Transient ممکن است به یک سرویس Scoped وابسته باشد. در این صورت، هر نمونه جدید از سرویس Transient، به همان نمونه Scoped موجود در محدوده فعلی دسترسی خواهد داشت.


ملاحظات و موارد احتیاط:

  • مصرف منابع: از آنجایی که در هر بار درخواست یک نمونه جدید ایجاد می‌شود، استفاده بیش از حد از سرویس‌های Transient برای سرویس‌های سنگین یا پرهزینه می‌تواند منجر به مصرف بالای منابع و کاهش عملکرد شود.
  • عدم اشتراک‌گذاری حالت: سرویس‌های Transient برای سناریوهایی مناسب هستند که نیازی به اشتراک‌گذاری حالت بین درخواست‌ها یا حتی در طول یک درخواست واحد وجود ندارد.

public interface IOperation
{
    string OperationId { get; }
}

public class Operation : IOperation
{
    public string OperationId { get; } = Guid.NewGuid().ToString();
}

بهترین موقعیت برای استفاده از این حالت وقتی است که در یک درخواست انتظار تغییر وضعیت ندارید. فرض کنید که سرویس زیر یک کُد یونیک تولید میکند:

public class UuidGeneratorService : IUuidGeneratorService
{
    public string GenerateUuid()
    {
        return Guid.NewGuid().ToString();
    }
}

اگر بصورت Transient ساخته شود، در یک درخواست مانند:

app.MapGet("/scoped-uuid", (IServiceProvider provider) =>
{
    using var scope = provider.CreateScope();
    var service1 = scope.ServiceProvider.GetRequiredService<IUuidGeneratorService>();
    var service2 = scope.ServiceProvider.GetRequiredService<IUuidGeneratorService>();

    return $"UUID 1: {service1.GenerateUuid()} | UUID 2: {service2.GenerateUuid()}";
});

مقادیر برگشتی یکی خواهد بود: (چون در یک درخواست ارسال شده است)

UUID 1: 9f3a5b81-7c62-40a7-b5a4-676cb74f3d5e | UUID 2: 9f3a5b81-7c62-40a7-b5a4-676cb74f3d5e

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

در Program.cs:

builder.Services.AddTransient();
در این مثال، هر بار که IOperation تزریق شود، یک نمونه جدید از Operation با یک OperationId منحصربه‌فرد ایجاد خواهد شد.

انتخاب طول عمر مناسب: ملاحظات کلیدی
انتخاب طول عمر مناسب برای یک سرویس در ASP.NET Core یک تصمیم مهم است که بر عملکرد، مدیریت منابع و رفتار کلی برنامه تأثیر می‌گذارد. در اینجا چند نکته کلیدی برای کمک به انتخاب آگاهانه آورده شده است:

  • نیاز به اشتراک‌گذاری حالت: اگر سرویس شما نیاز به اشتراک‌گذاری حالت بین تمام درخواست‌ها دارد، Singleton مناسب است (با احتیاط در مورد مسائل همزمانی). اگر اشتراک‌گذاری حالت فقط در طول یک درخواست HTTP مورد نیاز است، Scoped انتخاب بهتری است. اگر نیازی به اشتراک‌گذاری حالت نیست، Transient می‌تواند گزینه مناسبی باشد.
  • هزینه ایجاد و مدیریت منابع: سرویس‌های Singleton فقط یک بار ایجاد می‌شوند، بنابراین سربار ایجاد آن‌ها فقط یک بار اتفاق می‌افتد. سرویس‌های Scoped در هر درخواست ایجاد می‌شوند، و سرویس‌های Transient در هر بار درخواست. برای سرویس‌های سنگین و پرهزینه، ایجاد نمونه‌های متعدد می‌تواند بر عملکرد تأثیر بگذارد.
  • وابستگی‌ها: طول عمر یک سرویس می‌تواند تحت تأثیر طول عمر وابستگی‌های آن قرار بگیرد. به عنوان مثال، تزریق یک سرویس Scoped به یک سرویس Singleton می‌تواند منجر به رفتار غیرمنتظره شود، زیرا سرویس Singleton تنها یک بار ایجاد می‌شود و ممکن است سعی کند از یک نمونه قدیمی از وابستگی Scoped استفاده کند. به طور کلی، توصیه می‌شود که سرویس‌های با طول عمر کوتاه‌تر به سرویس‌های با طول عمر برابر یا طولانی‌تر وابسته باشند (Transient به Scoped یا Singleton، Scoped به Singleton).
  • سناریوی استفاده: ماهیت سرویس و نحوه استفاده از آن در برنامه باید در انتخاب طول عمر در نظر گرفته شود. سرویس‌های مربوط به پیکربندی سراسری معمولاً Singleton هستند، سرویس‌های مرتبط با درخواست کاربر معمولاً Scoped هستند و سرویس‌های انجام دهنده عملیات کوتاه‌مدت معمولاً Transient هستند.

بهترین شیوه‌ها و رهنمودها
پیش‌فرض Transient: اگر در مورد طول عمر یک سرویس مطمئن نیستید، Transient را به عنوان پیش‌فرض در نظر بگیرید. این امر از مشکلات اشتراک‌گذاری حالت ناخواسته جلوگیری می‌کند. سپس، در صورت نیاز به اشتراک‌گذاری حالت در سطح درخواست یا سراسری، به Scoped یا Singleton تغییر دهید.
آگاهی از همزمانی در Singleton: هنگام استفاده از سرویس‌های Singleton که حالت را مدیریت می‌کنند، اطمینان حاصل کنید که آن‌ها برای دسترسی همزمان ایمن هستند (به عنوان مثال، با استفاده از قفل‌ها یا ساختارهای داده‌ای thread-safe).
اجتناب از تزریق Scoped به Singleton: سعی کنید از تزریق سرویس‌های Scoped به سرویس‌های Singleton خودداری کنید، زیرا این می‌تواند منجر به استفاده از نمونه‌های قدیمی و مشکلات غیرمنتظره شود. اگر نیاز به انجام این کار دارید، ممکن است لازم باشد یک کارخانه (Factory) را به سرویس Singleton تزریق کنید تا در هر بار نیاز، یک نمونه جدید از سرویس Scoped دریافت کند.
استفاده آگاهانه از Singleton برای سرویس‌های سنگین: اگر یک سرویس Singleton منابع زیادی را مصرف می‌کند، در نظر بگیرید که آیا واقعاً نیاز است که در طول کل عمر برنامه در حافظه بماند. در برخی موارد، ممکن است یک رویکرد حافظه پنهان با انقضا (expiration) مناسب‌تر باشد.
تست و نظارت: پس از انتخاب طول عمر برای سرویس‌های خود، برنامه را به طور کامل تست کنید تا از رفتار مورد انتظار اطمینان حاصل کنید و عملکرد را نظارت کنید تا از هرگونه مشکل مربوط به مدیریت منابع جلوگیری شود.


نتیجه‌گیری
درک عمیق از طول عمر سرویس‌ها در ASP.NET Core برای ساخت برنامه‌های کاربردی قوی، مقیاس‌پذیر و قابل نگهداری ضروری است. انتخاب صحیح بین Singleton، Scoped و Transient تأثیر مستقیمی بر نحوه مدیریت منابع، اشتراک‌گذاری حالت و عملکرد کلی برنامه دارد. با در نظر گرفتن ماهیت سرویس، نیاز به اشتراک‌گذاری حالت، هزینه ایجاد و مدیریت منابع، و وابستگی‌ها، توسعه‌دهندگان می‌توانند تصمیمات آگاهانه‌ای بگیرند که منجر به برنامه‌های کاربردی کارآمدتر و قابل اعتمادتر می‌شود. تسلط بر این مفاهیم کلیدی، توسعه‌دهندگان را قادر می‌سازد تا از قدرت کامل سیستم تزریق وابستگی ASP.NET Core بهره‌مند شوند و برنامه‌هایی با معماری تمیز و انعطاف‌پذیر ایجاد کنند.

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

0 نظر

    هنوز نظری برای این مقاله ثبت نشده است.