تكرار السؤال: …
الإجابة المختصرة
الـ Transient ينشئ كائناً جديداً في كل طلب، والـ Scoped ينشئ كائناً واحداً لكل طلب HTTP/سياق عمل، والـ Singleton ينشئ نسخة واحدة لكل عمر التطبيق. وتحدث التبعية الأسيرة عندما تعتمد خدمة طويلة العمر على خدمة أقصر عمراً، وأخطرها Singleton يعتمد على Scoped، أو على Transient يحمل حالة أو موارد قابلة للتحرير (IDisposable).
الشرح
تعتبر التبعية الأسيرة (Captive Dependency) خطأ فادحاً يهدد أمان وتماسك البيانات: إذا تم حقن خدمة Scoped (مثل DbContext مخصص لطلب المستخدم الحالي) داخل خدمة من نوع Singleton، فسيتم أسر كائن قاعدة البيانات داخل الـ Singleton ولن يُحرر أبداً، مما يؤدي لتسريب الذاكرة (Memory Leak) ومشاركة سياق المستخدم الخاطئ بين طلبات مستخدمين مختلفين عبر الـ Threads!
مثال بالكود
builder.Services.AddScoped<AppDbContext>(); // one instance per HTTP request
builder.Services.AddSingleton<ReportCache>(); // one instance for the whole application
builder.Services.AddTransient<IClock, SystemClock>();
// BUG (captive dependency): the singleton captures a scoped DbContext for the life of the app
public class ReportCache(AppDbContext db) { } // ValidateScopes throws in Development
// FIX: create a short-lived scope whenever the singleton needs the scoped service
public class ReportCache(IServiceScopeFactory scopes)
{
public async Task RefreshAsync()
{
using var scope = scopes.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// use db only inside this scope
}
}أخطاء شائعة
- تسجيل خدمة تعتمد على سياق المستخدم والمصادقة كـ Singleton، مما يخلط بيانات العملاء ببعضها في وقت التشغيل.