عنوان:

‫پیکربندی هوشمند و تحلیل طول‌عمر وابستگی‌ها (DI Lifetimes) با GitHub Copilot


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۹ ۱۲:۳۷
آدرس: www.dntips.ir
در پروژه‌های بزرگ دات‌نت، ثبت دستی و مداوم اینترفیس‌ها و پیاده‌سازی‌ها در کانتینر وارونگی کنترل (IoC Container) نه‌تنها کاری تکراری است، بلکه انتخاب نادرست طول‌عمر (Lifetime) می‌تواند فجایع پنهانی مانند نشت حافظه (Memory Leaks)، تداخل‌های همزمانی (Race Conditions)، یا تزریق شیء دارای وضعیت معلق (Captive Dependencies) ایجاد کند. درخواست‌های شتاب‌زده مانند «این سرویس‌ها را رجیستر کن»، اغلب همه‌چیز را کورکورانه به شکل AddScoped یا AddTransient ثبت می‌کنند. راهکار مهندسی، وادار کردن Copilot به ارزیابی منطق کلاسی و بررسی دلایل انتخاب هر Lifetime قبل از تغییر کد است.

چالش اساسی: خطای Captive Dependency

یکی از خطرناک‌ترین باگ‌های معماری زمانی رخ می‌دهد که یک سرویس با طول‌عمر طولانی‌تر (Singleton)، یک سرویس با طول‌عمر کوتاه‌تر (Scoped) مانند DbContext را درون سازنده خود دریافت کند:
  • سرویس Scoped در حافظه Singleton به دام می‌افتد و تا پایان عمر اپلیکیشن آزاد نمی‌شود.
  • اتصالات پایگاه داده باز می‌مانند و کش‌های داخلی Entity Framework Core متورم شده و داده‌های کهنه (Stale Data) تولید می‌کنند.

ساختار پرامپت مهندسی برای تحلیل و ثبت وابستگی‌ها

هنگام گسترش پروژه یا بازنویسی بخش ثبت سرویس‌ها در Program.cs، از این الگوی تحلیلی استفاده کنید:
کلاس‌ها و اینترفیس‌های موجود در فایل‌های مشخص‌شده را تحلیل کن و پیشنهادهای ثبت در Dependency Injection را استخراج کن.
الزامات و مراحل تحلیل:
۱. فعلاً کدی را تغییر نده یا اضافه نکن.
۲. پیشنهادها را به سه دسته تفکیک کن:
  • Singleton: سرویس‌های کاملاً بدون وضعیت (Stateless)، عملیات سنگین مقداردهی اولیه یا کش‌های سراسری ایمن در برابر نخ‌ها (Thread-Safe).
  • Scoped: سرویس‌هایی که با داده‌های چرخه حیات یک درخواست (Per-Request) یا با لایه پایگاه داده (DbContext) سروکار دارند.
  • Transient: ابزارهای کم‌حجم، محاسباتی یا کامپوننت‌های سبک بدون وضعیت.
۳. برای هر مورد توضیح بده که چرا آن Lifetime خاص انتخاب شده است.
۴. هشدار بده که آیا در میان کلاس‌ها، ریسک بروز Captive Dependency یا استفاده از فیلدهای دارای وضعیت ناامن (Stateful Fields) وجود دارد یا خیر.

نمونه خروجی و دسته‌بندی استخراج‌شده

تحلیل خروجی Copilot ساختار سرویس‌ها را تفکیک و تثبیت می‌کند:
  • گروه Scoped: سرویس‌های OrderService و CustomerRepository: به دلیل وابستگی مستقیم به ApplicationDbContext و نیاز به جداسازی تراکنش‌ها به ازای هر درخواست HTTP.
  • گروه Singleton: سرویس‌های SystemClock یا MemoryCacheProvider: فاقد وضعیت متغیر بر اساس درخواست، پیاده‌سازی Thread-safe و هزینه بالای ساخت مجدد.
  • گروه Transient: سرویس‌های OrderValidator و TokenGeneratorHelper: سرویس‌های سبک و بدون وضعیت که نگه‌داری آنها در حافظه نیازی نیست.

طراحی متدهای الحاقی تمیز (Extension Methods) برای ثبت سرویس‌ها

پس از تأیید تحلیل، می‌توان از Copilot خواست کدهای ثبت را در قالب متدهای الحاقی لایه‌بندی‌شده و مرتب پیاده‌سازی کند:
namespace MyApp.Infrastructure.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddInfrastructureServices(
        this IServiceCollection services, 
        IConfiguration configuration)
    {
        // سرویس‌های Singleton بدون وابستگی به درخواست
        services.AddSingleton<IDateTimeProvider, SystemDateTimeProvider>();

        // لایه دسترسی به داده و سرویس‌های مرتبط (Scoped)
        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddScoped<IOrderService, OrderService>();

        // ابزارهای محاسباتی سبک (Transient)
        services.AddTransient<IOrderPricingCalculator, OrderPricingCalculator>();

        return services;
    }
}

نکات تکمیلی برای ثبت پیشرفته و مدرن وابستگی‌ها

  • استفاده از کتابخانه‌های اسکن خودکار اسمبلی (Scrutor): در پروژه‌های بسیار بزرگ، به جای ثبت دستی صدها کلاس، از Copilot بخواهید با پکیج Scrutor و الگوهای Conventions سرویس‌ها را به صورت خودکار رجیستر کند:
services.Scan(scan => scan
    .FromAssemblyOf<OrderService>()
    .AddClasses(classes => classes.Where(type => type.Name.EndsWith("Service")))
    .AsImplementedInterfaces()
    .WithScopedLifetime());
  • اعتبارسنجی Scopes در زمان راه‌اندازی (Validation on Build): در محیط توسعه، همیشه تنظیم ValidateScopes = true را در کانتینر ASP.NET Core فعال کنید تا بروز هرگونه Captive Dependency در لحظه بالا آمدن برنامه کشف شود.
  • ثبت با کلید در دات‌نت ۸+ (Keyed Services): برای سناریوهایی با چندین پیاده‌سازی از یک اینترفیس، از قابلیت Keyed DI (services.AddKeyedScoped) استفاده کنید.

قاعده کلیدی: Copilot می‌تواند تمام کاندیداهای ثبت را استخراج و دسته‌بندی کند، اما تعیین نهایی چرخه حیات (Lifetime) و مسئولیت رفتارهای چندنخی همواره بر عهده مهندس نرم‌افزار است.