📑 فهرست مطالب
+ +🔍 گام ۱: تحلیل ریشه خطا
+پیغام خطای DI به وضوح یک وابستگی چرخشی (Circular Dependency) را نشان میدهد:
+IXFileContentExtractor ➔ XVisionFileContentExtractor ➔ IXDefaultAIOCRService ➔ XDefaultAIOCRService ➔ XAIServiceBase ➔ IXFileContentExtractor
+ چرا این اتفاق افتاد؟
+-
+
- کلاس
XVisionFileContentExtractorبرای انجام OCR بهIXDefaultAIOCRServiceنیاز دارد.
+ - کلاس
XDefaultAIOCRServiceازXAIServiceBaseارثبری کرده است.
+ - سازنده (Constructor) کلاس
XAIServiceBaseبهIXFileContentExtractorنیاز دارد.
+
این زنجیره باعث میشود کانتینر DI در یک حلقه بینهایت گیر کند و نتواند هیچیک از این سرویسها را مقداردهی اولیه نماید.
+💡 گام ۲: استراتژی رفع خطا
+برای شکستن این چرخه، باید وابستگی XDefaultAIOCRService به XAIServiceBase را حذف کنیم.
سرویس OCR فقط نیاز به برقراری ارتباط با مدل زبانی (LLM) برای استخراج متن از تصویر دارد و به هیچوجه به IXFileContentExtractor، IXFileProvider یا IXAiDataProvider نیاز ندارد. بنابراین، با حذف ارثبری از XAIServiceBase و پیادهسازی مستقیم منطق ساخت IChatClient درون همین کلاس، چرخه وابستگی کاملاً شکسته میشود.
🛠️ گام ۳: اصلاح Interface سرویس OCR
+ابتدا باید ارثبری غیرضروری IXAiServiceBase را از اینترفیس حذف کنیم، زیرا این سرویس فقط وظیفه OCR را بر عهده دارد و نیازی به متدهای مدیریت پروژه، مکالمه و پیام ندارد.
using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using System.Collections.Generic;
+
+namespace xAiApi.Interfaces
+{
+ /// <summary>
+ /// سرویس اختصاصی برای انجام OCR بر روی محتوای تصویری ...
+ /// </summary>
+ public interface IXDefaultAIOCRService
+ {
+ /// <summary>
+ /// درخواست انجام OCR بر روی محتوای داده شده ...
+ /// </summary>
+ Task<string> RequestOCRAsync(
+ IList<ChatMessage> messages,
+ CancellationToken cancellationToken = default
+ );
+
+ /// <summary>
+ /// درخواست انجام OCR بر روی محتوای داده شده به صورت Stream ...
+ /// </summary>
+ IAsyncEnumerable<string> RequestOCRAsEnumerable(
+ IList<ChatMessage> messages,
+ CancellationToken cancellationToken = default
+ );
+ }
+}
+ ⚙️ گام ۴: بازنویسی مستقل کلاس XDefaultAIOCRService
+اکنون کلاس پیادهسازی را بازنویسی میکنیم تا دیگر از XAIServiceBase ارثبری نکند. منطق ساخت IChatClient (که قبلاً در کلاس پایه بود) به صورت مستقیم و تمیز درون این کلاس قرار میگیرد.
using System;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using OpenAI;
+using OllamaSharp;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using xAiApi.Configurations;
+using xAiApi.Constants;
+using xAiApi.Extensions;
+using xAiModels.Constants;
+using xAiModels.Extensions;
+using xCommons.Extensions;
+using xExceptions.Constants;
+
+namespace xAiApi.Providers
+{
+ /// <summary>
+ /// پیادهسازی مستقل سرویس OCR بدون وابستگی به XAIServiceBase ...
+ /// </summary>
+ public class XDefaultAIOCRService : IXDefaultAIOCRService
+ {
+ private readonly string prompt;
+ private readonly XAiModelDescriptor descriptor;
+ private readonly ILogger<XDefaultAIOCRService> logger;
+
+ public XDefaultAIOCRService(
+ ILogger<XDefaultAIOCRService> logger,
+ XAiApiConfiguration configuration,
+ string model = null,
+ string prompt = null
+ )
+ {
+ this.logger = logger;
+
+ // ۱. دریافت پیکربندی مدل
+ if (model.IsNullOrEmpty())
+ {
+ model = XAiApiConstants.XAiDefaultVisionModelName;
+ }
+
+ descriptor = configuration.GetModel(model);
+ if (descriptor.IsNullOrDefault())
+ {
+ XException.InvalidData.Throw("Invalid OCR Model Configuration");
+ }
+
+ // ۲. دریافت پرامپت استخراج
+ if (prompt.IsNullOrEmpty())
+ {
+ prompt = configuration.GetPrompt(
+ name: XAiApiConstants.XAiApiContentExtractionPromptName,
+ @params: null
+ );
+ }
+
+ this.prompt = prompt;
+ if (this.prompt.IsNullOrEmpty())
+ {
+ XException.InvalidArgs.Throw("OCR Prompt cannot be empty");
+ }
+ }
+
+ public virtual async Task<string> RequestOCRAsync(
+ IList<ChatMessage> messages,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (!messages.HasChild())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ using var client = GetClient();
+
+ var pMessage = new ChatMessage(ChatRole.System, prompt);
+ messages = [pMessage, .. messages];
+
+ var response = await client.GetResponseAsync(
+ messages: messages,
+ cancellationToken: cancellationToken
+ );
+
+ if (!response.IsValid())
+ {
+ XException.ActionFailed.Throw();
+ }
+
+ return response.Text;
+ }
+
+ public virtual async IAsyncEnumerable<string> RequestOCRAsEnumerable(
+ IList<ChatMessage> messages,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default
+ )
+ {
+ if (!messages.HasChild())
+ {
+ XException.InvalidArgs.Throw();
+ }
+
+ using var client = GetClient();
+
+ var pMessage = new ChatMessage(ChatRole.System, prompt);
+ messages = [pMessage, .. messages];
+
+ var enumerable = client.GetStreamingResponseAsync(
+ options: null,
+ messages: messages
+ );
+
+ await foreach (var res in enumerable)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ yield break;
+ }
+ yield return res.Text;
+ }
+ }
+
+ /// <summary>
+ /// ساخت کلاینت ارتباط با LLM (جایگزین متد GetClient در XAIServiceBase) ...
+ /// </summary>
+ private IChatClient GetClient()
+ {
+ var model = descriptor.LLM;
+ var apiKey = descriptor.ApiKey;
+ var url = new Uri(descriptor.Url);
+ var httpClient = new HttpClient { BaseAddress = url, Timeout = Timeout.InfiniteTimeSpan };
+
+ 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;
+ default:
+ XException.InvalidData.Throw($"Unsupported provider: {descriptor.Provider}");
+ break;
+ }
+
+ if (result == null)
+ {
+ XException.InvalidData.Throw("Failed to create Chat Client");
+ }
+
+ return result;
+ }
+ }
+}
+ 🔌 گام ۵: بررسی ثبت در Dependency Injection
+با توجه به تغییرات فوق، ثبت سرویس در فایل Startup.cs بدون هیچ تغییری به درستی کار خواهد کرد، زیرا امضای Constructor اکنون سادهتر شده و فقط به ILogger و XAiApiConfiguration وابسته است که هر دو از قبل در DI ثبت شدهاند.
+ خط
services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>(); در Startup.cs کاملاً معتبر است و دیگر باعث ایجاد چرخه وابستگی نمیشود.
+ خلاصه دستاوردهای این اصلاح:
+| مزیت | +توضیح | +
|---|---|
| 🚫 حذف Circular Dependency | +چرخه معیوب بین Extractor و سرویس OCR کاملاً شکسته شد. | +
| ⚡ افزایش عملکرد (Performance) | +سرویس OCR دیگر بار اضافی مقداردهی اولیه DataProvider و FileProvider را تحمل نمیکند. | +
| 🎯 اصل تکوظیفهای (SRP) | +کلاس XDefaultAIOCRService اکنون فقط و فقط مسئول ارتباط با مدل برای OCR است. |
+
| 🧪 قابلیت تستپذیری (Testability) | +تزریق وابستگیهای کمتر، نوشتن Unit Test برای این سرویس را بسیار سادهتر میکند. | +