454 lines
23 KiB
HTML
454 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fa" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>رفع خطای Scoped/Singleton Mismatch در DI - فن آوران ساحر علم</title>
|
||
<style>
|
||
:root {
|
||
--primary: #1e3a8a;
|
||
--secondary: #3b82f6;
|
||
--accent: #f59e0b;
|
||
--success: #10b981;
|
||
--danger: #ef4444;
|
||
--warning: #f97316;
|
||
--bg-light: #f8fafc;
|
||
--bg-code: #1e293b;
|
||
--text-dark: #0f172a;
|
||
--text-muted: #64748b;
|
||
--border: #e2e8f0;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
font-family: 'Tahoma', 'Segoe UI', sans-serif;
|
||
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 100%);
|
||
color: var(--text-dark);
|
||
line-height: 1.8;
|
||
padding: 20px;
|
||
}
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 16px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
}
|
||
.header {
|
||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||
color: white;
|
||
padding: 40px;
|
||
text-align: center;
|
||
}
|
||
.header h1 { font-size: 2.1em; margin-bottom: 10px; }
|
||
.header .subtitle { font-size: 1.1em; opacity: 0.95; }
|
||
.meta-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
background: var(--bg-light);
|
||
padding: 15px 30px;
|
||
border-bottom: 2px solid var(--border);
|
||
flex-wrap: wrap;
|
||
gap: 15px;
|
||
}
|
||
.meta-item { display: flex; align-items: center; gap: 8px; font-size: 0.9em; color: var(--text-muted); }
|
||
.meta-item strong { color: var(--primary); }
|
||
.content { padding: 40px; }
|
||
.section {
|
||
margin-bottom: 35px;
|
||
padding: 25px;
|
||
background: var(--bg-light);
|
||
border-radius: 12px;
|
||
border-right: 5px solid var(--secondary);
|
||
}
|
||
.section h2 {
|
||
color: var(--primary);
|
||
font-size: 1.5em;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 2px solid var(--border);
|
||
}
|
||
.section h3 { color: var(--secondary); font-size: 1.2em; margin: 20px 0 12px; }
|
||
pre {
|
||
background: var(--bg-code);
|
||
color: #e2e8f0;
|
||
padding: 18px;
|
||
border-radius: 8px;
|
||
overflow-x: auto;
|
||
direction: ltr;
|
||
text-align: left;
|
||
font-family: 'Consolas', monospace;
|
||
font-size: 0.85em;
|
||
margin: 15px 0;
|
||
border-right: 4px solid var(--accent);
|
||
}
|
||
code {
|
||
background: #fef3c7;
|
||
color: #92400e;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-family: 'Consolas', monospace;
|
||
font-size: 0.9em;
|
||
direction: ltr;
|
||
display: inline-block;
|
||
}
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 15px 0;
|
||
background: white;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
th { background: var(--primary); color: white; padding: 12px; text-align: right; }
|
||
td { padding: 12px; border-bottom: 1px solid var(--border); }
|
||
tr:hover { background: var(--bg-light); }
|
||
.alert { padding: 15px 20px; border-radius: 8px; margin: 15px 0; border-right: 4px solid; }
|
||
.alert-info { background: #dbeafe; border-color: var(--secondary); color: #1e40af; }
|
||
.alert-success { background: #d1fae5; border-color: var(--success); color: #065f46; }
|
||
.alert-danger { background: #fee2e2; border-color: var(--danger); color: #991b1b; }
|
||
.alert-warning { background: #fef3c7; border-color: var(--accent); color: #92400e; }
|
||
.footer { background: var(--primary); color: white; padding: 25px; text-align: center; }
|
||
.toc { background: white; padding: 20px; border-radius: 10px; margin-bottom: 25px; border: 2px solid var(--border); }
|
||
.toc h3 { color: var(--primary); margin-bottom: 15px; }
|
||
.toc ol { padding-right: 25px; }
|
||
.toc li { padding: 6px 0; }
|
||
.toc a { color: var(--secondary); text-decoration: none; }
|
||
.file-change { background: #f0f9ff; border-right: 4px solid var(--secondary); padding: 15px; margin: 10px 0; border-radius: 8px; }
|
||
.file-change .path { font-family: 'Consolas', monospace; color: var(--primary); font-weight: bold; direction: ltr; display: inline-block; }
|
||
.badge-modify { background: var(--warning); color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.75em; margin-right: 8px; }
|
||
.chain-diagram { background: white; padding: 25px; border-radius: 10px; margin: 20px 0; text-align: center; }
|
||
.chain-step { display: inline-block; background: var(--danger); color: white; padding: 10px 18px; border-radius: 8px; margin: 5px; font-size: 0.88em; }
|
||
.chain-step.fixed { background: var(--success); }
|
||
.chain-step.singleton { background: var(--primary); }
|
||
.chain-step.scoped { background: var(--warning); }
|
||
.chain-arrow { display: inline-block; color: var(--accent); font-size: 1.5em; margin: 0 8px; vertical-align: middle; }
|
||
.legend { display: flex; gap: 20px; justify-content: center; margin-top: 15px; flex-wrap: wrap; }
|
||
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 0.9em; }
|
||
.legend-box { width: 20px; height: 20px; border-radius: 4px; }
|
||
.diff-old { background: #fee2e2; color: #991b1b; padding: 2px 6px; border-radius: 3px; text-decoration: line-through; }
|
||
.diff-new { background: #d1fae5; color: #065f46; padding: 2px 6px; border-radius: 3px; font-weight: bold; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
|
||
<div class="header">
|
||
<h1>🔗 رفع خطای Scoped/Singleton Mismatch در DI</h1>
|
||
<div class="subtitle">تحلیل و رفع عدم تطابق طول عمر سرویسها در کانتینر Dependency Injection</div>
|
||
</div>
|
||
|
||
<div class="meta-bar">
|
||
<div class="meta-item">👨💻 <strong>توسعهدهنده:</strong> هادی خزاعی اصل</div>
|
||
<div class="meta-item">🏢 <strong>شرکت:</strong> فن آوران ساحر علم</div>
|
||
<div class="meta-item">📅 <strong>تاریخ:</strong> شنبه ۱۲ مهر ۱۴۰۵</div>
|
||
<div class="meta-item">📦 <strong>پروژه:</strong> xAiApi</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
|
||
<div class="toc">
|
||
<h3>📑 فهرست مطالب</h3>
|
||
<ol>
|
||
<li><a href="#analysis">تحلیل دقیق ریشه خطا</a></li>
|
||
<li><a href="#chain">زنجیره وابستگی مشکلساز</a></li>
|
||
<li><a href="#solution">راهحل اصلاحی</a></li>
|
||
<li><a href="#step1">گام ۱: اصلاح Startup.cs</a></li>
|
||
<li><a href="#step2">گام ۲: بررسی Thread Safety</a></li>
|
||
<li><a href="#verification">تأیید رفع کامل خطا</a></li>
|
||
</ol>
|
||
</div>
|
||
|
||
<!-- Section 1: Analysis -->
|
||
<div class="section" id="analysis">
|
||
<h2>🔍 گام ۱: تحلیل دقیق ریشه خطا</h2>
|
||
<p>پیغام خطای DI به یک مشکل کلاسیک در مدیریت طول عمر سرویسها اشاره میکند:</p>
|
||
|
||
<div class="alert alert-danger">
|
||
<strong>⚠️ پیغام خطا:</strong><br>
|
||
<code style="font-size: 0.95em;">Cannot consume scoped service 'IXDefaultAIOCRService' from singleton 'IXFileContentExtractor'</code>
|
||
</div>
|
||
|
||
<h3>قانون طلایی DI در .NET Core:</h3>
|
||
<table>
|
||
<tr>
|
||
<th>قانون</th>
|
||
<th>توضیح</th>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>✅ مجاز</strong></td>
|
||
<td>Singleton میتواند Singleton را مصرف کند</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>✅ مجاز</strong></td>
|
||
<td>Scoped میتواند Singleton را مصرف کند</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>✅ مجاز</strong></td>
|
||
<td>Transient میتواند هر چیزی را مصرف کند</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="background: var(--danger); color: white;"><strong>❌ ممنوع</strong></td>
|
||
<td><strong>Singleton نمیتواند Scoped را مصرف کند</strong> (خطای فعلی)</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="background: var(--danger); color: white;"><strong>❌ ممنوع</strong></td>
|
||
<td><strong>Singleton نمیتواند Transient را مصرف کند</strong></td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="alert alert-info">
|
||
<strong>💡 دلیل منطقی:</strong> اگر یک Singleton بتواند یک Scoped service را مصرف کند، آن Scoped service عملاً به یک Singleton تبدیل میشود (چون فقط یک بار ساخته شده و در تمام درخواستها استفاده میشود). این مسئله باعث نشت حافظه و رفتار غیرقابل پیشبینی میشود.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 2: Chain -->
|
||
<div class="section" id="chain">
|
||
<h2>🔗 گام ۲: زنجیره وابستگی مشکلساز</h2>
|
||
|
||
<h3>وضعیت فعلی (قبل از اصلاح):</h3>
|
||
<div class="chain-diagram">
|
||
<span class="chain-step singleton">XFileContentExtractor<br><small>(Singleton)</small></span>
|
||
<span class="chain-arrow">→</span>
|
||
<span class="chain-step singleton">XVisionFileContentExtractor<br><small>(Singleton)</small></span>
|
||
<span class="chain-arrow">→</span>
|
||
<span class="chain-step scoped">IXDefaultAIOCRService<br><small>(Scoped) ❌</small></span>
|
||
</div>
|
||
|
||
<div class="legend">
|
||
<div class="legend-item">
|
||
<div class="legend-box" style="background: var(--primary);"></div>
|
||
<span>Singleton</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-box" style="background: var(--warning);"></div>
|
||
<span>Scoped</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-box" style="background: var(--danger);"></div>
|
||
<span>خطا</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h3>بررسی وابستگیهای <code>XDefaultAIOCRService</code>:</h3>
|
||
<table>
|
||
<tr>
|
||
<th>وابستگی</th>
|
||
<th>نوع ثبت فعلی</th>
|
||
<th>آیا Stateful است؟</th>
|
||
</tr>
|
||
<tr>
|
||
<td><code>XAiApiConfiguration</code></td>
|
||
<td>Singleton</td>
|
||
<td>❌ خیر (فقط خواندنی)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>ILogger<XDefaultAIOCRService></code></td>
|
||
<td>Singleton</td>
|
||
<td>❌ خیر (Thread-safe)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>string model</code></td>
|
||
<td>Primitive</td>
|
||
<td>❌ خیر</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>string prompt</code></td>
|
||
<td>Primitive</td>
|
||
<td>❌ خیر</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="alert alert-success">
|
||
<strong>✅ نتیجهگیری کلیدی:</strong> کلاس <code>XDefaultAIOCRService</code> هیچ وابستگی Scoped یا Stateful ندارد و کاملاً Stateless است. بنابراین میتواند با خیال راحت به <strong>Singleton</strong> ارتقا یابد.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 3: Solution -->
|
||
<div class="section" id="solution">
|
||
<h2>💡 گام ۳: راهحل اصلاحی</h2>
|
||
<p>تنها یک تغییر کوچک در فایل <code>Startup.cs</code> لازم است: تغییر طول عمر <code>IXDefaultAIOCRService</code> از <code>Scoped</code> به <code>Singleton</code>.</p>
|
||
|
||
<h3>زنجیره اصلاحشده (بعد از تغییر):</h3>
|
||
<div class="chain-diagram">
|
||
<span class="chain-step fixed">XFileContentExtractor<br><small>(Singleton)</small></span>
|
||
<span class="chain-arrow">→</span>
|
||
<span class="chain-step fixed">XVisionFileContentExtractor<br><small>(Singleton)</small></span>
|
||
<span class="chain-arrow">→</span>
|
||
<span class="chain-step fixed">IXDefaultAIOCRService<br><small>(Singleton) ✅</small></span>
|
||
</div>
|
||
|
||
<div class="alert alert-info">
|
||
<strong>💡 چرا این راهحل امن است؟</strong>
|
||
<ul style="padding-right: 25px; margin-top: 10px;">
|
||
<li>✅ <code>XDefaultAIOCRService</code> هیچ State ای ندارد که بین درخواستها به اشتراک گذاشته شود</li>
|
||
<li>✅ <code>IChatClient</code> در هر فراخوانی <code>GetClient()</code> به صورت محلی ساخته و Dispose میشود</li>
|
||
<li>✅ <code>HttpClient</code> با <code>Timeout.InfiniteTimeSpan</code> thread-safe است</li>
|
||
<li>✅ <code>ILogger</code> ذاتاً thread-safe است</li>
|
||
<li>✅ <code>XAiApiConfiguration</code> فقط خواندنی و thread-safe است</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 4: Step 1 -->
|
||
<div class="section" id="step1">
|
||
<h2>🛠️ گام ۴: اصلاح Startup.cs</h2>
|
||
|
||
<div class="file-change">
|
||
<span class="badge-modify">MODIFY</span>
|
||
<span class="path">xAiApi/Startup.cs (متد ConfigureServices)</span>
|
||
</div>
|
||
|
||
<h3>کد اصلاحشده:</h3>
|
||
<pre>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)
|
||
<span class="diff-old">services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>();</span>
|
||
<span class="diff-new">services.AddSingleton<IXDefaultAIOCRService, XDefaultAIOCRService>();</span>
|
||
|
||
// سایر سرویسهای AI (Scoped باقی میمانند چون به DataProvider وابستهاند)
|
||
services.AddScoped<IXDefaultAiService, XDefaultAiService>();
|
||
services.AddScoped<IXDefaultEmbeddingService, XDefaultEmbeddingService>();
|
||
services.AddScoped<IXDefaultThinkingAiService, XDefaultThinkingAiService>();
|
||
}</pre>
|
||
|
||
<div class="alert alert-success">
|
||
<strong>✅ نکته مهم:</strong> فقط <code>IXDefaultAIOCRService</code> به Singleton تغییر میکند. سایر سرویسهای AI (<code>IXDefaultAiService</code>, <code>IXDefaultEmbeddingService</code>, <code>IXDefaultThinkingAiService</code>) به دلیل وابستگی به <code>IXAiDataProvider</code> و <code>IXFileProvider</code> که Scoped هستند، باید Scoped باقی بمانند.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 5: Thread Safety -->
|
||
<div class="section" id="step2">
|
||
<h2>🔒 گام ۵: بررسی Thread Safety</h2>
|
||
<p>با تبدیل <code>XDefaultAIOCRService</code> به Singleton، باید اطمینان حاصل کنیم که این کلاس Thread-safe است:</p>
|
||
|
||
<table>
|
||
<tr>
|
||
<th>بخش از کد</th>
|
||
<th>وضعیت Thread Safety</th>
|
||
<th>توضیح</th>
|
||
</tr>
|
||
<tr>
|
||
<td><code>Descriptor</code> (Property)</td>
|
||
<td><span style="color: var(--success);">✅ Safe</span></td>
|
||
<td>فقط در Constructor مقداردهی میشود و readonly است</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>Prompt</code> (Property)</td>
|
||
<td><span style="color: var(--success);">✅ Safe</span></td>
|
||
<td>فقط در Constructor مقداردهی میشود و readonly است</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>Options</code> (Property)</td>
|
||
<td><span style="color: var(--success);">✅ Safe</span></td>
|
||
<td>فقط در Constructor مقداردهی میشود</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>GetClient()</code></td>
|
||
<td><span style="color: var(--success);">✅ Safe</span></td>
|
||
<td>در هر فراخوانی یک <code>IChatClient</code> جدید میسازد (بدون State مشترک)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>GetHttpClient()</code></td>
|
||
<td><span style="color: var(--success);">✅ Safe</span></td>
|
||
<td>در هر فراخوانی یک <code>HttpClient</code> جدید میسازد</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>RequestOCRAsync()</code></td>
|
||
<td><span style="color: var(--success);">✅ Safe</span></td>
|
||
<td>از <code>using var client</code> استفاده میکند و State محلی دارد</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>RequestOCRAsEnumerable()</code></td>
|
||
<td><span style="color: var(--success);">✅ Safe</span></td>
|
||
<td>از <code>using var client</code> استفاده میکند و State محلی دارد</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="alert alert-success">
|
||
<strong>✅ نتیجه:</strong> کلاس <code>XDefaultAIOCRService</code> کاملاً Thread-safe است و میتواند با اطمینان کامل به عنوان Singleton ثبت شود.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 6: Verification -->
|
||
<div class="section" id="verification">
|
||
<h2>✅ گام ۶: تأیید رفع کامل خطا</h2>
|
||
|
||
<h3>خلاصه تغییرات:</h3>
|
||
<table>
|
||
<tr>
|
||
<th>فایل</th>
|
||
<th>تغییر</th>
|
||
<th>دلیل</th>
|
||
</tr>
|
||
<tr>
|
||
<td><code>Startup.cs</code></td>
|
||
<td><span class="diff-old">AddScoped</span> → <span class="diff-new">AddSingleton</span><br>برای <code>IXDefaultAIOCRService</code></td>
|
||
<td>رفع خطای Scoped/Singleton Mismatch</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<h3>چکلیست نهایی:</h3>
|
||
<div class="alert alert-success">
|
||
<ol style="padding-right: 25px;">
|
||
<li>✅ خطای <code>Cannot consume scoped service from singleton</code> رفع میشود</li>
|
||
<li>✅ تمام Extractor ها (Singleton) میتوانند <code>XVisionFileContentExtractor</code> را مصرف کنند</li>
|
||
<li>✅ <code>XVisionFileContentExtractor</code> میتواند <code>IXDefaultAIOCRService</code> را مصرف کند</li>
|
||
<li>✅ <code>XFileContentExtractor</code> (Composite) میتواند تمام Extractor ها را در Constructor دریافت کند</li>
|
||
<li>✅ Thread Safety کاملاً حفظ شده است</li>
|
||
<li>✅ Performance بهبود مییابد (ساخت یکبار به جای ساخت در هر Request)</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<h3>دستورالعمل اجرا:</h3>
|
||
<pre># ۱. فایل Startup.cs را باز کنید
|
||
# ۲. خط زیر را پیدا کنید:
|
||
services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>();
|
||
|
||
# ۳. آن را به این صورت تغییر دهید:
|
||
services.AddSingleton<IXDefaultAIOCRService, XDefaultAIOCRService>();
|
||
|
||
# ۴. پروژه را Rebuild کنید
|
||
dotnet build
|
||
|
||
# ۵. پروژه را اجرا کنید - خطای AggregateException دیگر ظاهر نخواهد شد</pre>
|
||
|
||
<div class="alert alert-info">
|
||
<strong>💡 نکته تکمیلی:</strong> پس از اعمال این تغییر، میتوانید با خیال راحت endpoint <code>ExtractContent</code> را در Controller پیادهسازی کنید، زیرا تمام زیرساخت DI اکنون به درستی پیکربندی شده است.
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<p><strong>👨💻 توسعهدهنده:</strong> هادی خزاعی اصل</p>
|
||
<p><strong>🏢 شرکت:</strong> فن آوران ساحر علم</p>
|
||
<p><strong>📅 تاریخ:</strong> شنبه ۱۲ مهر ۱۴۰۵</p>
|
||
<p style="margin-top: 15px; opacity: 0.8; font-size: 0.9em;">
|
||
🔗 مستند فنی رفع خطای Scoped/Singleton Mismatch در xAiApi - تمامی حقوق محفوظ است
|
||
</p>
|
||
</div>
|
||
|
||
</div>
|
||
</body>
|
||
</html> |