🔄 بازطراحی XFileContentExtractor بر پایه DI

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

🔍 گام ۱: تحلیل نواقص کد فعلی

در نسخه فعلی XFileContentExtractor، از مکانیزم Reflection (Assembly.LoadFrom و Activator.CreateInstance) برای یافتن و ساخت نمونه‌های Extractor استفاده شده است. این رویکرد دارای نواقص جدی زیر است:

❌ عدم پشتیبانی از وابستگی‌ها (DI Bypass)

کلاس‌هایی مانند XVisionFileContentExtractor یا XAudioFileContentExtractor دارای وابستگی‌هایی مانند IXDefaultAIOCRService یا XAiApiConfiguration هستند. Activator.CreateInstance نمی‌تواند این وابستگی‌ها را حل کند و باعث خطای MissingMethodException می‌شود.

❌ مشکل قفل‌شدگی فایل (File Locking)

استفاده از Assembly.LoadFrom در حلقه روی فایل‌های DLL می‌تواند باعث قفل شدن فایل‌ها در محیط‌های هاستینگ (مانند IIS) و جلوگیری از به‌روزرسانی یا Deploy شود.

❌ خطر حلقه بی‌نهایت (Circular Dependency)

اگر XFileContentExtractor خودش به عنوان IXFileContentExtractor در DI ثبت شود، ممکن است در لیست بازگردانده شود و باعث فراخوانی بازگشتی بی‌نهایت در متدهای CanExtract شود.

⚠️ نتیجه‌گیری: استفاده از Reflection برای ساخت اشیایی که وابستگی دارند، یک Anti-Pattern در .NET Core است. راه‌حل استاندارد، استفاده از قابلیت IEnumerable<T> در تزریق وابستگی است.

🛠️ گام ۲: بازنویسی کلاس XFileContentExtractor

به جای اسکن دستی DLL ها، از کانتینر DI می‌خواهیم تمام پیاده‌سازی‌های ثبت‌شده‌ی IXFileContentExtractor را به ما تزریق کند. سپس خودِ کامپوزیت را از لیست فیلتر می‌کنیم.

MODIFY xAiApi/Providers/Extractors/XFileContentExtractor.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using xAiApi.Interfaces.Extractors;
using xAiModels.Models;
using xCommons.Extensions;
using xExceptions.Constants;

namespace xAiApi.Providers.Extractors
{
    /// <summary>
    /// Composite extractor that delegates to appropriate extractor
    /// based on MIME type using Dependency Injection ...
    /// </summary>
    public class XFileContentExtractor : IXFileContentExtractor
    {
        private readonly IList<IXFileContentExtractor> extractors;

        /// <summary>
        /// Constructor: Inject all registered IXFileContentExtractor instances ...
        /// </summary>
        public XFileContentExtractor(IEnumerable<IXFileContentExtractor> availableExtractors)
        {
            // فیلتر کردن خودِ این کلاس برای جلوگیری از حلقه بی‌نهایت (Circular Dependency)
            extractors = availableExtractors
                .Where(e => e.GetType() != typeof(XFileContentExtractor))
                .ToList();
        }

        /// <summary>
        /// Check if any registered extractor supports the specified MIME type ...
        /// </summary>
        public bool CanExtract(string mimeType)
        {
            return extractors.Any(e => e.CanExtract(mimeType));
        }

        /// <summary>
        /// Extract text content from file stream ...
        /// </summary>
        public async Task<string> ExtractAsync(
            Stream fileStream,
            string mimeType,
            CancellationToken cancellationToken = default
        )
        {
            var extractor = extractors.FirstOrDefault(e => e.CanExtract(mimeType));
            
            if (extractor.IsNull())
            {
                XException.NotAllowed.Throw($"Unsupported file type: {mimeType}");
            }

            return await extractor.ExtractAsync(
                fileStream: fileStream,
                mimeType: mimeType,
                cancellationToken: cancellationToken
            );
        }

        /// <summary>
        /// Extract content from stream as Rich Result ...
        /// </summary>
        public async Task<XFileExtractionResult> ExtractRichAsync(
            Stream fileStream,
            string fileName,
            string mimeType,
            CancellationToken cancellationToken = default
        )
        {
            var extractor = extractors.FirstOrDefault(e => e.CanExtract(mimeType));
            
            if (extractor.IsNull())
            {
                XException.NotAllowed.Throw($"Unsupported file type: {mimeType}");
            }

            return await extractor.ExtractRichAsync(
                fileStream: fileStream,
                fileName: fileName,
                mimeType: mimeType,
                cancellationToken: cancellationToken
            );
        }
    }
}

🔌 گام ۳: ثبت صحیح سرویس‌ها در Startup.cs

برای اینکه تزریق IEnumerable<IXFileContentExtractor> کار کند، باید تمام Extractor های خاص را به صورت Singleton (چون State-less هستند) در کانتینر DI ثبت کنیم. کانتینر .NET Core به طور خودکار آن‌ها را در یک لیست جمع‌آوری می‌کند.

MODIFY xAiApi/Startup.cs (متد ConfigureServices)
public void ConfigureServices(IServiceCollection services)
{
    // ... (ثبت‌های قبلی سرویس‌ها)

    // ==========================================================
    // ✅ ثبت File Content Extractors (الگوی Composite)
    // ==========================================================
    
    // ۱. ثبت Extractor های پایه
    services.AddSingleton<IXFileContentExtractor, XPlainTextFileContentExtractor>();
    services.AddSingleton<IXFileContentExtractor, XDocxFileContentExtractor>();
    services.AddSingleton<IXFileContentExtractor, XExcelFileContentExtractor>();
    services.AddSingleton<IXFileContentExtractor, XPdfFileContentExtractor>();
    
    // ۲. ثبت Extractor های پیشرفته (تصویر و صوت)
    // نکته: XImageFileContentExtractor و XAudioFileContentExtractor باید در پروژه موجود باشند
    services.AddSingleton<IXFileContentExtractor, XImageFileContentExtractor>();
    services.AddSingleton<IXFileContentExtractor, XAudioFileContentExtractor>();
    
    // ۳. ثبت Vision Extractor (که وابستگی به IXDefaultAIOCRService دارد)
    // این خط به طور خودکار وابستگی‌های XVisionFileContentExtractor را از DI حل می‌کند
    services.AddSingleton<IXFileContentExtractor, XVisionFileContentExtractor>();

    // ۴. ثبت کامپوزیت اصلی (این کلاس لیست بالا را در Constructor دریافت می‌کند)
    services.AddSingleton<IXFileContentExtractor, XFileContentExtractor>();

    // ... (ثبت سرویس‌های AI و سایر موارد)
    services.AddScoped<IXDefaultAiService, XDefaultAiService>();
    services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>();
    services.AddScoped<IXDefaultEmbeddingService, XDefaultEmbeddingService>();
    services.AddScoped<IXDefaultThinkingAiService, XDefaultThinkingAiService>();
}
💡 نکته حیاتی درباره ترتیب ثبت:
در .NET Core، وقتی IEnumerable<T> را Inject می‌کنید، تمام ثبت‌های T (شامل خودِ XFileContentExtractor اگر قبل از فیلتر کردن باشد) را برمی‌گرداند. به همین دلیل در Constructor کلاس XFileContentExtractor، خط .Where(e => e.GetType() != typeof(XFileContentExtractor)) اضافه شده است تا از حلقه بی‌نهایت جلوگیری شود.

✨ گام ۴: مزایای رویکرد جدید

ویژگی رویکرد قدیمی (Reflection) رویکرد جدید (DI)
حل وابستگی‌ها (Dependencies) ❌ شکست می‌خورد ✅ به طور خودکار حل می‌شود
عملکرد (Performance) ⚠️ کند (اسکن DLL در هر بار) ✅ بسیار سریع (Resolved at startup)
قابلیت تست (Unit Testing) ❌ بسیار دشوار (Mocking سخت) ✅ آسان (تزریق لیست Mock)
پایداری در محیط Production ❌ خطر File Locking ✅ کاملاً پایدار و استاندارد
افزودن Extractor جدید خودکار (اما با ریسک) فقط افزودن یک خط AddSingleton در Startup.cs
✅ نتیجه‌گیری نهایی:
با این تغییر، معماری پروژه شما کاملاً با اصول SOLID (به ویژه Dependency Inversion) و الگوهای استاندارد .NET Core همسو می‌شود. کلاس XVisionFileContentExtractor که به IXDefaultAIOCRService وابسته است، اکنون بدون هیچ خطایی مقداردهی اولیه خواهد شد.