🔗 رفع خطای Scoped/Singleton Mismatch در DI

تحلیل و رفع عدم تطابق طول عمر سرویس‌ها در کانتینر Dependency Injection
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
🏢 شرکت: فن آوران ساحر علم
📅 تاریخ: شنبه ۱۲ مهر ۱۴۰۵
📦 پروژه: xAiApi

🔍 گام ۱: تحلیل دقیق ریشه خطا

پیغام خطای DI به یک مشکل کلاسیک در مدیریت طول عمر سرویس‌ها اشاره می‌کند:

⚠️ پیغام خطا:
Cannot consume scoped service 'IXDefaultAIOCRService' from singleton 'IXFileContentExtractor'

قانون طلایی DI در .NET Core:

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

🔗 گام ۲: زنجیره وابستگی مشکل‌ساز

وضعیت فعلی (قبل از اصلاح):

XFileContentExtractor
(Singleton)
→ XVisionFileContentExtractor
(Singleton)
→ IXDefaultAIOCRService
(Scoped) ❌
Singleton
Scoped
خطا

بررسی وابستگی‌های XDefaultAIOCRService:

وابستگی نوع ثبت فعلی آیا Stateful است؟
XAiApiConfiguration Singleton ❌ خیر (فقط خواندنی)
ILogger<XDefaultAIOCRService> Singleton ❌ خیر (Thread-safe)
string model Primitive ❌ خیر
string prompt Primitive ❌ خیر
✅ نتیجه‌گیری کلیدی: کلاس XDefaultAIOCRService هیچ وابستگی Scoped یا Stateful ندارد و کاملاً Stateless است. بنابراین می‌تواند با خیال راحت به Singleton ارتقا یابد.

💡 گام ۳: راه‌حل اصلاحی

تنها یک تغییر کوچک در فایل Startup.cs لازم است: تغییر طول عمر IXDefaultAIOCRService از Scoped به Singleton.

زنجیره اصلاح‌شده (بعد از تغییر):

XFileContentExtractor
(Singleton)
→ XVisionFileContentExtractor
(Singleton)
→ IXDefaultAIOCRService
(Singleton) ✅
💡 چرا این راه‌حل امن است؟
  • ✅ XDefaultAIOCRService هیچ State ای ندارد که بین درخواست‌ها به اشتراک گذاشته شود
  • ✅ IChatClient در هر فراخوانی GetClient() به صورت محلی ساخته و Dispose می‌شود
  • ✅ HttpClient با Timeout.InfiniteTimeSpan thread-safe است
  • ✅ ILogger ذاتاً thread-safe است
  • ✅ XAiApiConfiguration فقط خواندنی و thread-safe است

🛠️ گام ۴: اصلاح Startup.cs

MODIFY xAiApi/Startup.cs (متد ConfigureServices)

کد اصلاح‌شده:

public void ConfigureServices(IServiceCollection services)
{
    // ... (ثبت‌های قبلی)

    // ==========================================================
    // ✅ ثبت File Content Extractors (همگی Singleton)
    // ==========================================================
    services.AddSingleton<IXFileContentExtractor, XPdfFileContentExtractor>();
    services.AddSingleton<IXFileContentExtractor, XDocxFileContentExtractor>();
    services.AddSingleton<IXFileContentExtractor, XImageFileContentExtractor>();
    services.AddSingleton<IXFileContentExtractor, XAudioFileContentExtractor>();
    services.AddSingleton<IXFileContentExtractor, XExcelFileContentExtractor>();
    services.AddSingleton<IXFileContentExtractor, XVisionFileContentExtractor>();
    services.AddSingleton<IXFileContentExtractor, XPlainTextFileContentExtractor>();
    
    // ✅ کامپوزیت اصلی (لیست بالا را در Constructor دریافت می‌کند)
    services.AddSingleton<IXFileContentExtractor, XFileContentExtractor>();

    // ==========================================================
    // ✅ ثبت سرویس‌های AI
    // ==========================================================
    
    // ✅ تغییر از AddScoped به AddSingleton (رفع خطای DI)
    services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>();
    services.AddSingleton<IXDefaultAIOCRService, XDefaultAIOCRService>();
    
    // سایر سرویس‌های AI (Scoped باقی می‌مانند چون به DataProvider وابسته‌اند)
    services.AddScoped<IXDefaultAiService, XDefaultAiService>();
    services.AddScoped<IXDefaultEmbeddingService, XDefaultEmbeddingService>();
    services.AddScoped<IXDefaultThinkingAiService, XDefaultThinkingAiService>();
}
✅ نکته مهم: فقط IXDefaultAIOCRService به Singleton تغییر می‌کند. سایر سرویس‌های AI (IXDefaultAiService, IXDefaultEmbeddingService, IXDefaultThinkingAiService) به دلیل وابستگی به IXAiDataProvider و IXFileProvider که Scoped هستند، باید Scoped باقی بمانند.

🔒 گام ۵: بررسی Thread Safety

با تبدیل XDefaultAIOCRService به Singleton، باید اطمینان حاصل کنیم که این کلاس Thread-safe است:

بخش از کد وضعیت Thread Safety توضیح
Descriptor (Property) ✅ Safe فقط در Constructor مقداردهی می‌شود و readonly است
Prompt (Property) ✅ Safe فقط در Constructor مقداردهی می‌شود و readonly است
Options (Property) ✅ Safe فقط در Constructor مقداردهی می‌شود
GetClient() ✅ Safe در هر فراخوانی یک IChatClient جدید می‌سازد (بدون State مشترک)
GetHttpClient() ✅ Safe در هر فراخوانی یک HttpClient جدید می‌سازد
RequestOCRAsync() ✅ Safe از using var client استفاده می‌کند و State محلی دارد
RequestOCRAsEnumerable() ✅ Safe از using var client استفاده می‌کند و State محلی دارد
✅ نتیجه: کلاس XDefaultAIOCRService کاملاً Thread-safe است و می‌تواند با اطمینان کامل به عنوان Singleton ثبت شود.

✅ گام ۶: تأیید رفع کامل خطا

خلاصه تغییرات:

فایل تغییر دلیل
Startup.cs AddScoped → AddSingleton
برای IXDefaultAIOCRService
رفع خطای Scoped/Singleton Mismatch

چک‌لیست نهایی:

  1. ✅ خطای Cannot consume scoped service from singleton رفع می‌شود
  2. ✅ تمام Extractor ها (Singleton) می‌توانند XVisionFileContentExtractor را مصرف کنند
  3. ✅ XVisionFileContentExtractor می‌تواند IXDefaultAIOCRService را مصرف کند
  4. ✅ XFileContentExtractor (Composite) می‌تواند تمام Extractor ها را در Constructor دریافت کند
  5. ✅ Thread Safety کاملاً حفظ شده است
  6. ✅ Performance بهبود می‌یابد (ساخت یکبار به جای ساخت در هر Request)

دستورالعمل اجرا:

# ۱. فایل Startup.cs را باز کنید
# ۲. خط زیر را پیدا کنید:
services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>();

# ۳. آن را به این صورت تغییر دهید:
services.AddSingleton<IXDefaultAIOCRService, XDefaultAIOCRService>();

# ۴. پروژه را Rebuild کنید
dotnet build

# ۵. پروژه را اجرا کنید - خطای AggregateException دیگر ظاهر نخواهد شد
💡 نکته تکمیلی: پس از اعمال این تغییر، می‌توانید با خیال راحت endpoint ExtractContent را در Controller پیاده‌سازی کنید، زیرا تمام زیرساخت DI اکنون به درستی پیکربندی شده است.