Skip to content

Commit db47b4f

Browse files
JaJaJaJa
authored andcommitted
refactor: DocumentConversionService 重命名為 DocumentConverter 靜態類別、HostFactory 參數封裝、SonarCloud 程式碼品質修復、測試覆蓋率大幅擴展
1. DocumentConversionService → DocumentConverter 重構 - DocumentConversionService.cs 重命名為 DocumentConverter.cs - 改為純靜態方法設計,移除實例化需求 - ExtensionSessionBridge.cs:移除 DocumentConversionService 依賴注入,改用 DocumentConverter 靜態呼叫 - ConvertDocumentTool.cs:所有轉換方法改為 DocumentConverter 靜態呼叫 - HostFactory.cs:移除 DocumentConversionService 服務註冊 2. HostFactory 參數封裝優化 - 新增 HostConfigBundle record 封裝所有配置物件(ServerConfig、TransportConfig、SessionConfig、AuthConfig、TrackingConfig、OriginConfig、ExtensionConfig) - CreateStdioHost、CreateHttpHost、CreateWebSocketHost 方法簽章簡化,改接收單一 bundle 參數 - 減少方法參數數量,提升程式碼可讀性 3. SonarCloud 程式碼品質修復 - Extension.cs:使用 ObjectDisposedException.ThrowIf() 替代手動 if + throw - Extension.cs:OperationCanceledException、TimeoutException catch 區塊傳遞 exception 至 LogWarning - ExtensionConfig.cs:ValidateTempDirectory 使用 LINQ Any() 替代 foreach 迴圈 - ExtensionManager.cs:新增靜態 JsonOptions 快取 JsonSerializerOptions 避免重複建立 - ExtensionManager.cs:processCleanupManager?.Dispose() 簡化條件式呼叫 - ExtensionManager.cs:TimeoutException、OperationCanceledException catch 區塊傳遞 exception 至 LogWarning/LogDebug - ExtensionSessionBridge.cs:_pendingModifications.AddOrUpdate lambda 參數 `_` 改為 `key` 消除未使用參數警告 - ExtensionSessionBridge.cs:ObjectDisposedException、OperationCanceledException catch 區塊傳遞 exception 至 LogDebug - FileTransport.cs:DirectoryNotFoundException catch 區塊傳遞 exception 至 LogWarning - ProcessCleanupManager.cs:CreateJobObject、SetInformationJobObject、AssignProcessToJobObject 加入 SuppressMessage 抑制 SYSLIB1054 警告 4. 測試覆蓋率擴展 - 新增 DocumentConverterTests.cs(約 1100 行):完整覆蓋 GetMimeType、IsFormatSupported、ConvertToBytes、IsWordDocument、IsExcelDocument、IsPowerPointDocument、IsPdfDocument、IsPdfConvertibleFormat、IsImageFormat、ConvertWordDocument、ConvertExcelDocument、ConvertPowerPointDocument、ConvertPdfDocument、ConvertPdfToImages、ConvertToPdfFromSpecialFormat 等所有靜態方法 - 刪除 DocumentConversionServiceTests.cs(配合類別重命名) - ExtensionMetadataTests.cs:新增 VerifyChecksum_NullData_NonZeroChecksum_ReturnsFalse、VerifyChecksum_EmptyData_NonZeroChecksum_ReturnsFalse、CreateCommand_ReturnsValidCommandMetadata、CreateCommand_WithoutPayload_ReturnsValidCommandMetadata、CreateCommand_GeneratesUniqueCommandIds - ExtensionManagerTests.cs:新增靜態 TestJsonOptions 快取 JsonSerializerOptions - GenerateBarcodeHandlerTests.cs(新增約 470 行): - Execute_CreatesOutputDirectoryIfNotExists 目錄自動建立測試 - QRCODE、Code39Standard、Code39Extended、EAN8、EAN13、UPC-A、UPC-E、ISBN、ISSN 條碼類型測試 - Codabar、ITF14、Interleaved2of5、Standard2of5、DataMatrix、PDF417、AztecCode 條碼類型測試 - MaxiCode、PatchCode、DataLogic2of5、Pharmacode、OPC、IATA2of5 條碼類型測試 - BMP、GIF、EMF、SVG 輸出格式測試 - RecognizeBarcodeHandlerTests.cs(新增約 470 行): - CreateEmptyImage 輔助方法 - all、AllSupportedTypes、QRCODE 解碼類型別名測試 - Code39Standard、Code39Extended、EAN8、EAN13、UPC-A、UPC-E 辨識測試 - Codabar、Code93、ITF14、DataMatrix、PDF417、Aztec 辨識測試 - MaxiCode、MicroQR、DotCode、PostNet 辨識測試 - HighPerformance、HighQuality 精度模式測試 - MultipleBarcodes、NoBarcode、EmptyImage 邊界測試 - SetPdfPropertiesHandlerTests.cs(新增約 120 行): - Execute_ReturnsSuccessMessage 結果訊息驗證 - Execute_WithAllEmptyValues_DoesNotFail、Execute_WithAllNullValues_DoesNotFail 邊界測試 - 特殊字元(<, >, &, ', ")、Unicode、超長文字、換行符號屬性值測試 - CreationDate、ModDate 日期格式測試 - GetPdfSignaturesHandlerTests.cs(新增約 130 行): - CreatePdfWithMultipleSignatureFields 輔助方法 - Execute_WithSignatureField_ReturnsSignatureInfo 簽章資訊測試 - Execute_WithMultipleSignatureFields_ReturnsAllSignatures 多簽章測試 - Execute_WithUnsignedField_ReturnsInvalidSignature 未簽署欄位測試 - 空文檔、多頁簽章欄位測試 - ProcessCleanupManagerTests.cs:配合 SuppressMessage 屬性調整 5. 文檔更新 - docs/extensions.html:新增「實際範例:aspose-mcp-preview」章節 - docs/extensions.html:新增 aspose-mcp-preview 功能展示 GIF、安裝配置範例、命令列選項表格 - docs/extensions.html:更新最佳實踐(優先使用 stdin 傳輸模式、mmap 用於最佳效能) - docs/extensions.html:相關資源新增 aspose-mcp-preview GitHub 連結 - 新增 docs/images/aspose-mcp-preview-demo.gif 示範影像
1 parent 3bcdc3a commit db47b4f

20 files changed

+2624
-458
lines changed

Core/Conversion/DocumentConversionService.cs renamed to Core/Conversion/DocumentConverter.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ public class ConversionOptions
6868

6969
/// <summary>
7070
/// Provides document conversion functionality for various Aspose document types.
71-
/// This service is shared between Extension system and Conversion tools.
71+
/// This utility class is shared between Extension system and Conversion tools.
7272
/// </summary>
73-
public class DocumentConversionService
73+
public static class DocumentConverter
7474
{
7575
/// <summary>
7676
/// MIME type mappings for output formats.
@@ -340,7 +340,7 @@ public static Aspose.Pdf.SaveFormat GetPdfSaveFormat(string format)
340340
/// <returns>A MemoryStream containing the converted document.</returns>
341341
/// <exception cref="ArgumentNullException">Thrown when document is null.</exception>
342342
/// <exception cref="ArgumentException">Thrown when the output format is not supported for the document type.</exception>
343-
public Stream ConvertToStream(object document, DocumentType documentType, string outputFormat,
343+
public static Stream ConvertToStream(object document, DocumentType documentType, string outputFormat,
344344
ConversionOptions? options = null)
345345
{
346346
ArgumentNullException.ThrowIfNull(document);
@@ -379,7 +379,7 @@ public Stream ConvertToStream(object document, DocumentType documentType, string
379379
/// <returns>A byte array containing the converted document.</returns>
380380
/// <exception cref="ArgumentNullException">Thrown when document is null.</exception>
381381
/// <exception cref="ArgumentException">Thrown when the output format is not supported for the document type.</exception>
382-
public byte[] ConvertToBytes(object document, DocumentType documentType, string outputFormat,
382+
public static byte[] ConvertToBytes(object document, DocumentType documentType, string outputFormat,
383383
ConversionOptions? options = null)
384384
{
385385
using var stream = ConvertToStream(document, documentType, outputFormat, options);
@@ -955,7 +955,7 @@ public static string ConvertToPdfFromSpecialFormat(string inputPath, string outp
955955
/// </summary>
956956
/// <param name="outputFormat">The output format (e.g., "pdf", "html", "png").</param>
957957
/// <returns>The MIME type string, or "application/octet-stream" if not found.</returns>
958-
public string GetMimeType(string outputFormat)
958+
public static string GetMimeType(string outputFormat)
959959
{
960960
var format = NormalizeExtension(outputFormat);
961961
return MimeTypes.GetValueOrDefault(format, "application/octet-stream");
@@ -967,7 +967,7 @@ public string GetMimeType(string outputFormat)
967967
/// <param name="documentType">The document type to check.</param>
968968
/// <param name="outputFormat">The output format to check.</param>
969969
/// <returns><c>true</c> if the format is supported; otherwise, <c>false</c>.</returns>
970-
public bool IsFormatSupported(DocumentType documentType, string outputFormat)
970+
public static bool IsFormatSupported(DocumentType documentType, string outputFormat)
971971
{
972972
var format = NormalizeExtension(outputFormat);
973973
return SupportedFormats.TryGetValue(documentType, out var formats) && formats.Contains(format);
@@ -978,7 +978,7 @@ public bool IsFormatSupported(DocumentType documentType, string outputFormat)
978978
/// </summary>
979979
/// <param name="documentType">The document type.</param>
980980
/// <returns>An enumerable of supported format strings.</returns>
981-
public IEnumerable<string> GetSupportedFormats(DocumentType documentType)
981+
public static IEnumerable<string> GetSupportedFormats(DocumentType documentType)
982982
{
983983
return SupportedFormats.TryGetValue(documentType, out var formats)
984984
? formats

Core/Extension/Extension.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -424,8 +424,7 @@ public async Task<bool> EnsureStartedAsync()
424424
/// </exception>
425425
public async Task PerformHandshakeAsync(CancellationToken cancellationToken = default)
426426
{
427-
if (_disposed)
428-
throw new ObjectDisposedException(nameof(Extension));
427+
ObjectDisposedException.ThrowIf(_disposed, this);
429428

430429
if (_state != ExtensionState.Starting &&
431430
_state != ExtensionState.Initializing &&
@@ -1123,9 +1122,9 @@ public async Task StopAsync(bool resetRestartCount = false)
11231122
_stdinLock.Release();
11241123
}
11251124
}
1126-
catch (OperationCanceledException)
1125+
catch (OperationCanceledException ex)
11271126
{
1128-
_logger.LogWarning("Extension {ExtensionId} did not exit gracefully, killing process",
1127+
_logger.LogWarning(ex, "Extension {ExtensionId} did not exit gracefully, killing process",
11291128
_definition.Id);
11301129
if (stdinLockAcquired)
11311130
try
@@ -1854,9 +1853,9 @@ private async Task CleanupProcessAsync()
18541853
{
18551854
await _stdoutReaderTask.WaitAsync(readerTaskTimeout);
18561855
}
1857-
catch (TimeoutException)
1856+
catch (TimeoutException ex)
18581857
{
1859-
_logger.LogWarning(
1858+
_logger.LogWarning(ex,
18601859
"stdout reader task for extension {ExtensionId} did not complete within {Timeout}s. " +
18611860
"This may indicate the process is hung or streams are blocked.",
18621861
_definition.Id, ReaderTaskCleanupTimeoutSeconds);
@@ -1875,9 +1874,9 @@ private async Task CleanupProcessAsync()
18751874
{
18761875
await _stderrReaderTask.WaitAsync(readerTaskTimeout);
18771876
}
1878-
catch (TimeoutException)
1877+
catch (TimeoutException ex)
18791878
{
1880-
_logger.LogWarning(
1879+
_logger.LogWarning(ex,
18811880
"stderr reader task for extension {ExtensionId} did not complete within {Timeout}s",
18821881
_definition.Id, ReaderTaskCleanupTimeoutSeconds);
18831882
}
@@ -2049,9 +2048,9 @@ private async Task<bool> WriteToStdinWithTimeoutAsync(
20492048
await process.StandardInput.FlushAsync(timeoutCts.Token);
20502049
return true;
20512050
}
2052-
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
2051+
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
20532052
{
2054-
_logger.LogWarning(
2053+
_logger.LogWarning(ex,
20552054
"Stdin write to extension {ExtensionId} timed out after {Timeout}ms",
20562055
_definition.Id, _config.StdinWriteTimeoutMs);
20572056
return false;

Core/Extension/ExtensionConfig.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -617,12 +617,12 @@ private static void ValidateTempDirectory(string tempDir)
617617
.ToLowerInvariant();
618618

619619
var forbiddenPaths = GetForbiddenPaths();
620-
foreach (var forbidden in forbiddenPaths)
621-
if (normalizedPath.Equals(forbidden, StringComparison.OrdinalIgnoreCase) ||
622-
normalizedPath.StartsWith(forbidden + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
623-
throw new InvalidOperationException(
624-
$"TempDirectory cannot be set to system directory: {tempDir}. " +
625-
"Please use a user-writable directory like the system temp folder.");
620+
if (forbiddenPaths.Any(forbidden =>
621+
normalizedPath.Equals(forbidden, StringComparison.OrdinalIgnoreCase) ||
622+
normalizedPath.StartsWith(forbidden + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
623+
throw new InvalidOperationException(
624+
$"TempDirectory cannot be set to system directory: {tempDir}. " +
625+
"Please use a user-writable directory like the system temp folder.");
626626

627627
try
628628
{

Core/Extension/ExtensionManager.cs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ namespace AsposeMcpServer.Core.Extension;
1111
/// </summary>
1212
public class ExtensionManager : IHostedService, IAsyncDisposable
1313
{
14+
/// <summary>
15+
/// Cached JSON serializer options for deserializing extension configuration.
16+
/// </summary>
17+
private static readonly JsonSerializerOptions JsonOptions = new()
18+
{
19+
PropertyNameCaseInsensitive = true
20+
};
21+
1422
/// <summary>
1523
/// Valid transport modes supported by the system.
1624
/// </summary>
@@ -162,8 +170,7 @@ public ExtensionManager(
162170
{
163171
fileTransport?.Dispose();
164172
mmapTransport?.Dispose();
165-
if (processCleanupManager != null)
166-
processCleanupManager.Dispose();
173+
processCleanupManager?.Dispose();
167174
throw;
168175
}
169176
}
@@ -191,9 +198,9 @@ public async ValueTask DisposeAsync()
191198
{
192199
// Ignore cancellation during shutdown
193200
}
194-
catch (TimeoutException)
201+
catch (TimeoutException ex)
195202
{
196-
_logger.LogDebug("Health check task did not complete within timeout during disposal");
203+
_logger.LogDebug(ex, "Health check task did not complete within timeout during disposal");
197204
}
198205

199206
if (_initializationTask != null)
@@ -205,9 +212,9 @@ public async ValueTask DisposeAsync()
205212
{
206213
// Ignore cancellation during shutdown
207214
}
208-
catch (TimeoutException)
215+
catch (TimeoutException ex)
209216
{
210-
_logger.LogDebug("Background initialization task did not complete within timeout during disposal");
217+
_logger.LogDebug(ex, "Background initialization task did not complete within timeout during disposal");
211218
}
212219

213220
_healthCheckCts?.Dispose();
@@ -220,9 +227,9 @@ public async ValueTask DisposeAsync()
220227
{
221228
await Task.WhenAll(restartTasks).WaitAsync(TimeSpan.FromSeconds(10));
222229
}
223-
catch (TimeoutException)
230+
catch (TimeoutException ex)
224231
{
225-
_logger.LogWarning("Timed out waiting for restart tasks during disposal");
232+
_logger.LogWarning(ex, "Timed out waiting for restart tasks during disposal");
226233
}
227234
catch (Exception ex)
228235
{
@@ -460,10 +467,7 @@ private async Task LoadExtensionDefinitionsAsync(CancellationToken cancellationT
460467
try
461468
{
462469
var json = await File.ReadAllTextAsync(_config.ConfigPath, cancellationToken);
463-
var configFile = JsonSerializer.Deserialize<ExtensionsConfigFile>(json, new JsonSerializerOptions
464-
{
465-
PropertyNameCaseInsensitive = true
466-
});
470+
var configFile = JsonSerializer.Deserialize<ExtensionsConfigFile>(json, JsonOptions);
467471

468472
if (configFile?.Extensions == null || configFile.Extensions.Count == 0)
469473
{
@@ -587,7 +591,7 @@ private async Task<bool> InitializeSingleExtensionAsync(
587591
}
588592
catch (OperationCanceledException ex)
589593
{
590-
_logger.LogWarning("Handshake timeout for extension {Id}", definition.Id);
594+
_logger.LogWarning(ex, "Handshake timeout for extension {Id}", definition.Id);
591595
definition.IsAvailable = false;
592596
definition.UnavailableReason = $"Handshake timeout: {ex.Message}";
593597
return false;
@@ -1080,9 +1084,9 @@ private async Task PerformSingleHealthCheckAsync(
10801084
if (extension.State == ExtensionState.Idle)
10811085
await extension.SendHeartbeatAsync(timeoutCts.Token);
10821086
}
1083-
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
1087+
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
10841088
{
1085-
_logger.LogWarning(
1089+
_logger.LogWarning(ex,
10861090
"Health check for extension {Id} timed out",
10871091
extension.Definition.Id);
10881092
}

Core/Extension/ExtensionSessionBridge.cs

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,6 @@ public sealed class ExtensionSessionBridge : IDisposable
8080
/// </summary>
8181
private readonly TimeSpan _conversionCacheTtl;
8282

83-
/// <summary>
84-
/// Document conversion service for format conversion.
85-
/// </summary>
86-
private readonly DocumentConversionService _conversionService;
87-
8883
/// <summary>
8984
/// Debounce delay for session modifications.
9085
/// </summary>
@@ -154,19 +149,16 @@ public sealed class ExtensionSessionBridge : IDisposable
154149
/// <param name="config">Extension configuration.</param>
155150
/// <param name="sessionManager">Document session manager.</param>
156151
/// <param name="extensionManager">Extension manager.</param>
157-
/// <param name="conversionService">Document conversion service.</param>
158152
/// <param name="logger">Logger instance.</param>
159153
public ExtensionSessionBridge(
160154
ExtensionConfig config,
161155
DocumentSessionManager sessionManager,
162156
ExtensionManager extensionManager,
163-
DocumentConversionService conversionService,
164157
ILogger<ExtensionSessionBridge> logger)
165158
{
166159
_config = config;
167160
_sessionManager = sessionManager;
168161
_extensionManager = extensionManager;
169-
_conversionService = conversionService;
170162
_logger = logger;
171163

172164
_debounceDelay = TimeSpan.FromMilliseconds(config.DebounceDelayMs);
@@ -347,16 +339,16 @@ private void HandleSessionModified(string sessionId, SessionIdentity requestor)
347339

348340
_pendingModifications.AddOrUpdate(
349341
sessionId,
350-
_ =>
342+
key =>
351343
{
352344
var timer = new Timer(
353345
OnDebounceTimerElapsed,
354-
sessionId,
346+
key,
355347
_debounceDelay,
356348
Timeout.InfiniteTimeSpan);
357349
return (requestor, timer);
358350
},
359-
(_, existing) =>
351+
(key, existing) =>
360352
{
361353
try
362354
{
@@ -366,7 +358,7 @@ private void HandleSessionModified(string sessionId, SessionIdentity requestor)
366358
{
367359
var newTimer = new Timer(
368360
OnDebounceTimerElapsed,
369-
sessionId,
361+
key,
370362
_debounceDelay,
371363
Timeout.InfiniteTimeSpan);
372364
return (requestor, newTimer);
@@ -419,9 +411,9 @@ private void OnDebounceTimerElapsed(object? state)
419411
{
420412
await OnSessionModifiedAsync(sessionId, pending.Requestor, cancellationToken);
421413
}
422-
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
414+
catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
423415
{
424-
_logger.LogDebug(
416+
_logger.LogDebug(ex,
425417
"Session modified handling cancelled for session {SessionId}",
426418
sessionId);
427419
}
@@ -514,9 +506,9 @@ private void HandleSessionClosed(string sessionId, SessionIdentity owner)
514506
await extension.NotifySessionClosedAsync(sessionId, ConvertOwner(owner), cancellationToken);
515507
}
516508
}
517-
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
509+
catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
518510
{
519-
_logger.LogDebug(
511+
_logger.LogDebug(ex,
520512
"Session closed notification cancelled for session {SessionId}",
521513
sessionId);
522514
}
@@ -579,7 +571,7 @@ public async Task<BindingResult> BindAsync(
579571
ExtensionErrorCode.FormatNotSupported,
580572
$"Extension '{extensionId}' cannot handle document type '{session.Type}' with format '{outputFormat}'");
581573

582-
if (!_conversionService.IsFormatSupported(session.Type, outputFormat))
574+
if (!DocumentConverter.IsFormatSupported(session.Type, outputFormat))
583575
return BindingResult.Failure(
584576
ExtensionErrorCode.FormatNotSupported,
585577
$"Format '{outputFormat}' is not supported for document type '{session.Type}'");
@@ -788,7 +780,7 @@ public async Task<BindingResult> SetFormatAsync(
788780
if (session == null)
789781
return BindingResult.Failure(ExtensionErrorCode.SessionNotFound, $"Session not found: {sessionId}");
790782

791-
if (!_conversionService.IsFormatSupported(session.Type, newFormat))
783+
if (!DocumentConverter.IsFormatSupported(session.Type, newFormat))
792784
return BindingResult.Failure(
793785
ExtensionErrorCode.FormatNotSupported,
794786
$"Format '{newFormat}' is not supported for document type '{session.Type}'");
@@ -1281,7 +1273,7 @@ private void DrainStaleLocks(int targetCount)
12811273
try
12821274
{
12831275
var data = await session.ExecuteAsync(
1284-
doc => _conversionService.ConvertToBytes(doc, session.Type, outputFormat, options),
1276+
doc => DocumentConverter.ConvertToBytes(doc, session.Type, outputFormat, options),
12851277
cancellationToken);
12861278

12871279
if (!_closedSessions.ContainsKey(session.SessionId))
@@ -1292,16 +1284,16 @@ private void DrainStaleLocks(int targetCount)
12921284

12931285
return data;
12941286
}
1295-
catch (ObjectDisposedException)
1287+
catch (ObjectDisposedException ex)
12961288
{
1297-
_logger.LogDebug(
1289+
_logger.LogDebug(ex,
12981290
"Session {SessionId} was closed during conversion to {Format}",
12991291
session.SessionId, outputFormat);
13001292
return null;
13011293
}
1302-
catch (OperationCanceledException)
1294+
catch (OperationCanceledException ex)
13031295
{
1304-
_logger.LogDebug(
1296+
_logger.LogDebug(ex,
13051297
"Conversion of session {SessionId} to {Format} was cancelled",
13061298
session.SessionId, outputFormat);
13071299
return null;
@@ -1390,7 +1382,7 @@ private ExtensionMetadata CreateMetadata(DocumentSession session, string outputF
13901382
DocumentType = session.Type.ToString().ToLowerInvariant(),
13911383
OriginalPath = session.Path,
13921384
OutputFormat = outputFormat,
1393-
MimeType = _conversionService.GetMimeType(outputFormat),
1385+
MimeType = DocumentConverter.GetMimeType(outputFormat),
13941386
Timestamp = DateTime.UtcNow,
13951387
Owner = ConvertOwner(session.Owner)
13961388
};

0 commit comments

Comments
 (0)