🔗 رفع خطای Circular Dependency

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

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

پیغام خطای DI به وضوح یک حلقه وابستگی (Circular Dependency) را نشان می‌دهد:

IXFileContentExtractor → XVisionFileContentExtractor → IXDefaultAIOCRService → XDefaultAIOCRService → IXFileContentExtractor ❌
⚠️ تحلیل زنجیره:
  1. XVisionFileContentExtractor در constructor خود به IXDefaultAIOCRService نیاز دارد.
  2. XDefaultAIOCRService در constructor خود به IXFileContentExtractor نیاز دارد.
  3. اما IXFileContentExtractor همان کامپوزیتی است که شامل XVisionFileContentExtractor می‌باشد!
  4. این یک حلقه بی‌نهایت ایجاد می‌کند و DI Container قادر به ساخت هیچ‌یک از این سرویس‌ها نیست.

🎯 گام ۲: شناسایی علت اصلی

با بررسی دقیق کد XDefaultAIOCRService، یک نکته کلیدی کشف شد:

❌ پارامتر غیرضروری در Constructor

در constructor کلاس XDefaultAIOCRService، پارامتر IXFileContentExtractor fileContentExtractor دریافت می‌شود، اما در هیچ جای کلاس از آن استفاده نمی‌شود!

✅ سرویس OCR مستقل است

سرویس OCR فقط نیاز به ارتباط با مدل زبانی (LLM) دارد و به IXFileContentExtractor نیازی ندارد. این پارامتر به اشتباه از نسخه قبلی باقی مانده است.

کد مشکل‌دار فعلی:

public XDefaultAIOCRService(
    XAiApiConfiguration configuration,
    ILogger<XDefaultAIOCRService> logger,
    IXFileContentExtractor fileContentExtractor,  // ❌ پارامتر غیرضروری
    string model = null,
    string prompt = null
)
{
    // ... هیچ استفاده‌ای از fileContentExtractor در body کلاس نیست
}

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

برای شکستن حلقه وابستگی، کافی است پارامتر غیرضروری IXFileContentExtractor را از constructor کلاس XDefaultAIOCRService حذف کنیم.

IXFileContentExtractor → XVisionFileContentExtractor → IXDefaultAIOCRService → XDefaultAIOCRService ✅
✅ نتیجه: با حذف این پارامتر، زنجیره وابستگی کاملاً شکسته می‌شود و DI Container می‌تواند تمام سرویس‌ها را بدون مشکل مقداردهی اولیه کند.

🛠️ گام ۴: اصلاح XDefaultAIOCRService

MODIFY xAiApi/Providers/XDefaultAIOCRService.cs

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

using System;
using OpenAI;
using OllamaSharp;
using System.Net.Http;
using System.Threading;
using xAiModels.Models;
using xAiApi.Constants;
using xAiApi.Interfaces;
using xAiApi.Extensions;
using System.ClientModel;
using xCommons.Extensions;
using xAiModels.Extensions;
using xAiApi.Configurations;
using xExceptions.Constants;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using System.ClientModel.Primitives;
using System.Runtime.CompilerServices;

namespace xAiApi.Providers
{
    public class XDefaultAIOCRService : IXDefaultAIOCRService
    {
        /// <summary>
        /// Chat Options ...
        /// </summary>
        public ChatOptions Options { get; }

        /// <summary>
        /// Prompt ...
        /// </summary>
        public string Prompt { get; }

        /// <summary>
        /// Descriptor of Models which used in Service ...
        /// </summary>
        public XAiModelDescriptor Descriptor { get; }

        public XDefaultAIOCRService(
            XAiApiConfiguration configuration,
            ILogger<XDefaultAIOCRService> logger,
            // ✅ پارامتر IXFileContentExtractor حذف شد
            string model = null,
            string prompt = null
        )
        {
            //
            // Prepare Model Descriptor ...
            Descriptor = configuration.GetModel(model);
            if (!Descriptor.IsValid())
            {
                XException.InvalidConfiguration.Throw();
            }
            //
            Prompt = prompt;
            if (Prompt.IsNullOrEmpty())
            {
                XException.InvalidArgs.Throw();
            }
            //
            // Prepare Chat Options based On Tools ...
            Options = new ChatOptions();
        }

        //
        #region OCR ...
        /// <summary>
        /// Request for Doing OCR on Given Data Contents ...
        /// </summary>
        public virtual async Task<string> RequestOCRAsync(
            IList<ChatMessage> messages,
            CancellationToken cancellationToken = default
        )
        {
            //
            // Validate ...
            if (!messages.HasChild())
            {
                XException.InvalidArgs.Throw();
            }
            //
            using var client = GetClient();
            //
            // Preparing Extraction Prompt Message ...
            var pMessage = new ChatMessage(
                ChatRole.System,
                Prompt
            );
            //
            messages = [pMessage, .. messages];
            //
            var response = await client.GetResponseAsync(
                messages: messages,
                cancellationToken: cancellationToken
            );
            //
            // Validate Response ...
            if (!response.IsValid())
            {
                //
                // Dispose Client ...
                client.Dispose();
                XException.ActionFailed.Throw();
            }
            //
            // Retrieve Response Text ...
            var result = response.Text;
            //
            return result;
        }

        /// <summary>
        /// Request for Doing OCR on Given Data Contents as Stream ...
        /// </summary>
        public virtual async IAsyncEnumerable<string> RequestOCRAsEnumerable(
            IList<ChatMessage> messages,
            [EnumeratorCancellation]
            CancellationToken cancellationToken = default
        )
        {
            //
            // Validate ...
            if (!messages.HasChild())
            {
                XException.InvalidArgs.Throw();
            }
            //
            using var client = GetClient();
            //
            // Preparing Extraction Prompt Message ...
            var pMessage = new ChatMessage(
                ChatRole.System,
                Prompt
            );
            //
            messages = [pMessage, .. messages];
            //
            var enumerable = client.GetStreamingResponseAsync(
                options: null,
                messages: messages
            );
            //
            await foreach (var res in enumerable)
            {
                //
                // Cancellation Token ...
                if (cancellationToken.IsCancellationRequested)
                {
                    yield break;
                }
                //
                yield return res.Text;
            }
        }
        #endregion

        //
        #region Preaprations ...
        /// <summary>
        /// Get LLM Client instance for Communicating with LLM ...
        /// </summary>
        public virtual IChatClient GetClient()
        {
            //
            // Try to Initialize LLm ...
            var model = Descriptor.LLM;
            var apiKey = Descriptor.ApiKey;
            var url = new Uri(Descriptor.Url);
            var httpClient = GetHttpClient(Descriptor.Url);
            //
            IChatClient result = null;
            switch (Descriptor.Provider)
            {
                //
                case XAiModelProviderType.Ollama:
                    //
                    var ollamaClient = new OllamaApiClient(httpClient, model);
                    result =
                        new ChatClientBuilder(ollamaClient)
                        .UseFunctionInvocation()
                        .Build();
                    break;
                //
                case XAiModelProviderType.OpenAI:
                    //
                    var openAiClient = new OpenAIClient(
                        new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey),
                        new OpenAIClientOptions
                        {
                            Endpoint = url,
                            Transport = new HttpClientPipelineTransport(httpClient)
                        }
                    );
                    result =
                        new ChatClientBuilder(
                            openAiClient
                            .GetChatClient(model)
                            .AsIChatClient())
                        .UseFunctionInvocation()
                        .Build();
                    break;
                //
                case XAiModelProviderType.DeepSeek:
                    break;
                //
                case XAiModelProviderType.HuggingFace:
                    break;
                //
                default:
                    break;
            }
            //
            if (result.IsNullOrDefault())
            {
                XException.InvalidData.Throw();
            }
            //
            return result;
        }

        /// <summary>
        /// Create Custom HttpClient for Communicating with LLM API ...
        /// </summary>
        public virtual HttpClient GetHttpClient(string url = null)
        {
            //
            var result = new HttpClient
            {
                //
                // Disable timeout completely (not recommended for production)
                Timeout = Timeout.InfiniteTimeSpan,
                //
                BaseAddress = url.IsNullOrEmpty()
                    ? null
                    : new Uri(url),
            };
            //
            return result;
        }
        #endregion
    }
}

🔌 گام ۵: بررسی Startup.cs

با توجه به تغییرات فوق، ثبت سرویس‌ها در Startup.cs بدون هیچ تغییری به درستی کار خواهد کرد. اما برای اطمینان، ترتیب ثبت سرویس‌ها را بررسی می‌کنیم:

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

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

    // ✅ ثبت سرویس OCR (دیگر به IXFileContentExtractor وابسته نیست)
    services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>();

    // ✅ ثبت سایر سرویس‌های AI
    services.AddScoped<IXDefaultAiService, XDefaultAiService>();
    services.AddScoped<IXDefaultEmbeddingService, XDefaultEmbeddingService>();
    services.AddScoped<IXDefaultThinkingAiService, XDefaultThinkingAiService>();
}
💡 نکته مهم: ترتیب ثبت سرویس‌ها در DI Container اهمیت ندارد، زیرا .NET Core DI Container به صورت خودکار وابستگی‌ها را حل می‌کند. مهم این است که تمام سرویس‌های مورد نیاز ثبت شده باشند.

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

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

فایل تغییر دلیل
XDefaultAIOCRService.cs REMOVE حذف پارامتر IXFileContentExtractor پارامتر غیرضروری بود و باعث Circular Dependency می‌شد
Startup.cs NO CHANGE بدون تغییر ثبت سرویس‌ها به درستی انجام شده است

مزایای این اصلاح:

🚫 حذف Circular Dependency

  • حلقه وابستگی کاملاً شکسته شد
  • DI Container می‌تواند تمام سرویس‌ها را بسازد

⚡ افزایش Performance

  • سرویس OCR دیگر بار اضافی ندارد
  • تزریق وابستگی‌های کمتر = ساخت سریع‌تر

🎯 اصل تک‌وظیفه‌ای (SRP)

  • سرویس OCR فقط مسئول ارتباط با LLM است
  • وابستگی‌های غیرضروری حذف شدند

🧪 قابلیت تست‌پذیری

  • Unit Test ساده‌تر شد
  • Mock کردن وابستگی‌ها آسان‌تر است
✅ نتیجه نهایی:

با حذف پارامتر غیرضروری IXFileContentExtractor از constructor کلاس XDefaultAIOCRService، خطای Circular Dependency کاملاً رفع می‌شود و پروژه بدون مشکل اجرا خواهد شد.

پس از اعمال این تغییر، پروژه را rebuild کنید و خطای System.AggregateException دیگر ظاهر نخواهد شد.