From 5167f044b078bcfc7fbdda9f9432f423501fae0b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 09:17:28 +0000
Subject: [PATCH 1/9] Initial plan
From e51e54f4fdc8adf4947623feefb3d7863ca7d11d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 09:23:08 +0000
Subject: [PATCH 2/9] Add DTOs and ExtensionService for query and download
operations
Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com>
---
.../DTOs/DownloadExtensionDTO.cs | 20 ++
.../DTOs/ExtensionDTO.cs | 123 +++++++
.../DTOs/ExtensionQueryDTO.cs | 53 +++
.../DTOs/HttpResponseDTO.cs | 111 +++++++
.../DTOs/PagedResultDTO.cs | 47 +++
.../GeneralExtensionHost.cs | 17 +-
.../GeneralUpdate.Extension/IExtensionHost.cs | 5 +
.../Services/ExtensionService.cs | 307 ++++++++++++++++++
.../Services/IExtensionService.cs | 32 ++
9 files changed, 714 insertions(+), 1 deletion(-)
create mode 100644 src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs
create mode 100644 src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs
create mode 100644 src/c#/GeneralUpdate.Extension/DTOs/ExtensionQueryDTO.cs
create mode 100644 src/c#/GeneralUpdate.Extension/DTOs/HttpResponseDTO.cs
create mode 100644 src/c#/GeneralUpdate.Extension/DTOs/PagedResultDTO.cs
create mode 100644 src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs
create mode 100644 src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs
diff --git a/src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs
new file mode 100644
index 00000000..261b5a95
--- /dev/null
+++ b/src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs
@@ -0,0 +1,20 @@
+using System.IO;
+
+namespace GeneralUpdate.Extension.DTOs
+{
+ ///
+ /// Download extension file data transfer object
+ ///
+ public class DownloadExtensionDTO
+ {
+ ///
+ /// File name with extension
+ ///
+ public string FileName { get; set; } = null!;
+
+ ///
+ /// File stream
+ ///
+ public Stream Stream { get; set; } = null!;
+ }
+}
diff --git a/src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs
new file mode 100644
index 00000000..8bf80cfe
--- /dev/null
+++ b/src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+
+namespace GeneralUpdate.Extension.DTOs
+{
+ ///
+ /// Extension data transfer object
+ ///
+ public class ExtensionDTO
+ {
+ ///
+ /// Extension unique identifier
+ ///
+ public string Id { get; set; } = null!;
+
+ ///
+ /// Extension name (unique identifier, lowercase, no spaces)
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Human-readable display name of the extension
+ ///
+ public string? DisplayName { get; set; }
+
+ ///
+ /// Extension version
+ ///
+ public string? Version { get; set; }
+
+ ///
+ /// File size in bytes
+ ///
+ public long? FileSize { get; set; }
+
+ ///
+ /// Upload timestamp
+ ///
+ public DateTime? UploadTime { get; set; }
+
+ ///
+ /// Extension status (false-Disabled, true-Enabled)
+ ///
+ public bool? Status { get; set; }
+
+ ///
+ /// Extension description
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// File format/extension
+ ///
+ public string? Format { get; set; }
+
+ ///
+ /// File hash (SHA256)
+ ///
+ public string? Hash { get; set; }
+
+ ///
+ /// Publisher identifier
+ ///
+ public string? Publisher { get; set; }
+
+ ///
+ /// License identifier (e.g., "MIT", "Apache-2.0")
+ ///
+ public string? License { get; set; }
+
+ ///
+ /// Extension categories
+ ///
+ public List? Categories { get; set; }
+
+ ///
+ /// Supported platforms
+ ///
+ public Metadata.TargetPlatform SupportedPlatforms { get; set; } = Metadata.TargetPlatform.All;
+
+ ///
+ /// Minimum host application version required
+ ///
+ public string? MinHostVersion { get; set; }
+
+ ///
+ /// Maximum host application version supported
+ ///
+ public string? MaxHostVersion { get; set; }
+
+ ///
+ /// Release date and time for this version
+ ///
+ public DateTime? ReleaseDate { get; set; }
+
+ ///
+ /// List of extension IDs (Guids) that this extension depends on
+ ///
+ public List? Dependencies { get; set; }
+
+ ///
+ /// Pre-release flag
+ ///
+ public bool IsPreRelease { get; set; }
+
+ ///
+ /// Download URL for the extension package
+ ///
+ public string? DownloadUrl { get; set; }
+
+ ///
+ /// Custom properties for extension-specific metadata
+ ///
+ public Dictionary? CustomProperties { get; set; }
+
+ ///
+ /// Indicates whether the extension is compatible with the requested host version.
+ /// Null if no host version was specified in the query.
+ /// True if compatible, False if incompatible.
+ ///
+ public bool? IsCompatible { get; set; }
+ }
+}
diff --git a/src/c#/GeneralUpdate.Extension/DTOs/ExtensionQueryDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/ExtensionQueryDTO.cs
new file mode 100644
index 00000000..7f6e38a0
--- /dev/null
+++ b/src/c#/GeneralUpdate.Extension/DTOs/ExtensionQueryDTO.cs
@@ -0,0 +1,53 @@
+namespace GeneralUpdate.Extension.DTOs
+{
+ ///
+ /// Extension query data transfer object
+ ///
+ public class ExtensionQueryDTO
+ {
+ ///
+ /// Page number for pagination (default: 1)
+ ///
+ public int PageNumber { get; set; } = 1;
+
+ ///
+ /// Page size for pagination (default: 10)
+ ///
+ public int PageSize { get; set; } = 10;
+
+ ///
+ /// Filter by extension name (optional)
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Filter by publisher (optional)
+ ///
+ public string? Publisher { get; set; }
+
+ ///
+ /// Filter by category (optional)
+ ///
+ public string? Category { get; set; }
+
+ ///
+ /// Filter by target platform (optional)
+ ///
+ public Metadata.TargetPlatform? TargetPlatform { get; set; }
+
+ ///
+ /// Host version for compatibility checking (optional)
+ ///
+ public string? HostVersion { get; set; }
+
+ ///
+ /// Include pre-release versions (default: false)
+ ///
+ public bool IncludePreRelease { get; set; } = false;
+
+ ///
+ /// Search term for general search (optional)
+ ///
+ public string? SearchTerm { get; set; }
+ }
+}
diff --git a/src/c#/GeneralUpdate.Extension/DTOs/HttpResponseDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/HttpResponseDTO.cs
new file mode 100644
index 00000000..adf5f337
--- /dev/null
+++ b/src/c#/GeneralUpdate.Extension/DTOs/HttpResponseDTO.cs
@@ -0,0 +1,111 @@
+namespace GeneralUpdate.Extension.DTOs
+{
+ ///
+ /// Base HTTP response data transfer object
+ ///
+ public class HttpResponseDTO
+ {
+ ///
+ /// HTTP status code
+ ///
+ public int Code { get; set; }
+
+ ///
+ /// Response message
+ ///
+ public string Message { get; set; } = string.Empty;
+
+ ///
+ /// Creates a success response
+ ///
+ /// Success message
+ /// Success response
+ public static HttpResponseDTO Success(string message = "Success")
+ {
+ return new HttpResponseDTO
+ {
+ Code = 200,
+ Message = message
+ };
+ }
+
+ ///
+ /// Creates an internal server error response
+ ///
+ /// Error message
+ /// Error response
+ public static HttpResponseDTO InnerException(string message)
+ {
+ return new HttpResponseDTO
+ {
+ Code = 500,
+ Message = message
+ };
+ }
+ }
+
+ ///
+ /// Generic HTTP response data transfer object with body
+ ///
+ /// Body type
+ public sealed class HttpResponseDTO : HttpResponseDTO
+ {
+ ///
+ /// Response body
+ ///
+#nullable disable
+ public T Body { get; set; }
+#nullable restore
+
+ private HttpResponseDTO()
+ { }
+
+ ///
+ /// Creates a success response with data
+ ///
+ /// Response data
+ /// Success message
+ /// Success response
+ public static HttpResponseDTO Success(T data, string message = "Success")
+ {
+ return new HttpResponseDTO
+ {
+ Code = 200,
+ Body = data,
+ Message = message
+ };
+ }
+
+ ///
+ /// Creates a failure response
+ ///
+ /// Failure message
+ /// Optional response data
+ /// Failure response
+ public static HttpResponseDTO Failure(string message, T data = default)
+ {
+ return new HttpResponseDTO
+ {
+ Code = 400,
+ Body = data,
+ Message = message
+ };
+ }
+
+ ///
+ /// Creates an internal server error response
+ ///
+ /// Error message
+ /// Optional response data
+ /// Error response
+ public static HttpResponseDTO InnerException(string message, T data = default)
+ {
+ return new HttpResponseDTO
+ {
+ Code = 500,
+ Body = data,
+ Message = message
+ };
+ }
+ }
+}
diff --git a/src/c#/GeneralUpdate.Extension/DTOs/PagedResultDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/PagedResultDTO.cs
new file mode 100644
index 00000000..2893c1cb
--- /dev/null
+++ b/src/c#/GeneralUpdate.Extension/DTOs/PagedResultDTO.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace GeneralUpdate.Extension.DTOs
+{
+ ///
+ /// Paginated result data transfer object
+ ///
+ /// Item type
+ public class PagedResultDTO
+ {
+ ///
+ /// Current page number
+ ///
+ public int PageNumber { get; set; }
+
+ ///
+ /// Page size
+ ///
+ public int PageSize { get; set; }
+
+ ///
+ /// Total number of items
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// Total number of pages
+ ///
+ public int TotalPages { get; set; }
+
+ ///
+ /// Items in current page
+ ///
+ public IEnumerable Items { get; set; } = Enumerable.Empty();
+
+ ///
+ /// Whether there is a previous page
+ ///
+ public bool HasPrevious => PageNumber > 1;
+
+ ///
+ /// Whether there is a next page
+ ///
+ public bool HasNext => PageNumber < TotalPages;
+ }
+}
diff --git a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs
index 846dfbb8..e50f047a 100644
--- a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs
+++ b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs
@@ -18,6 +18,7 @@ public class GeneralExtensionHost : IExtensionHost
private readonly Download.IUpdateQueue _updateQueue;
private readonly Download.ExtensionDownloadService _downloadService;
private readonly Installation.ExtensionInstallService _installService;
+ private readonly Services.IExtensionService _extensionService;
private bool _globalAutoUpdateEnabled = true;
#region Properties
@@ -42,6 +43,11 @@ public bool GlobalAutoUpdateEnabled
set => _globalAutoUpdateEnabled = value;
}
+ ///
+ /// Gets the extension service for query and download operations.
+ ///
+ public Services.IExtensionService ExtensionService => _extensionService;
+
#endregion
#region Events
@@ -109,6 +115,13 @@ public GeneralExtensionHost(
_downloadService = new Download.ExtensionDownloadService(downloadPath, _updateQueue, downloadTimeout);
_installService = new Installation.ExtensionInstallService(installBasePath);
+ // Initialize extension service with empty list (will be updated via ParseAvailableExtensions)
+ _extensionService = new Services.ExtensionService(
+ new List(),
+ hostVersion,
+ _validator,
+ _downloadService);
+
// Wire up event handlers
_updateQueue.StateChanged += (sender, args) => UpdateStateChanged?.Invoke(sender, args);
_downloadService.ProgressUpdated += (sender, args) => DownloadProgress?.Invoke(sender, args);
@@ -164,7 +177,9 @@ public void LoadInstalledExtensions()
/// A list of parsed available extensions.
public List ParseAvailableExtensions(string json)
{
- return _catalog.ParseAvailableExtensions(json);
+ var extensions = _catalog.ParseAvailableExtensions(json);
+ _extensionService.UpdateAvailableExtensions(extensions);
+ return extensions;
}
///
diff --git a/src/c#/GeneralUpdate.Extension/IExtensionHost.cs b/src/c#/GeneralUpdate.Extension/IExtensionHost.cs
index 90fed8a3..d3f2859b 100644
--- a/src/c#/GeneralUpdate.Extension/IExtensionHost.cs
+++ b/src/c#/GeneralUpdate.Extension/IExtensionHost.cs
@@ -27,6 +27,11 @@ public interface IExtensionHost
///
bool GlobalAutoUpdateEnabled { get; set; }
+ ///
+ /// Gets the extension service for query and download operations.
+ ///
+ Services.IExtensionService ExtensionService { get; }
+
#endregion
#region Events
diff --git a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs
new file mode 100644
index 00000000..90d94b98
--- /dev/null
+++ b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs
@@ -0,0 +1,307 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using GeneralUpdate.Extension.DTOs;
+using GeneralUpdate.Extension.Metadata;
+
+namespace GeneralUpdate.Extension.Services
+{
+ ///
+ /// Implementation of extension query and download operations
+ ///
+ public class ExtensionService : IExtensionService
+ {
+ private List _availableExtensions;
+ private readonly Version? _hostVersion;
+ private readonly Compatibility.ICompatibilityValidator? _validator;
+ private readonly Download.ExtensionDownloadService? _downloadService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// List of available extensions
+ /// Optional host version for compatibility checking
+ /// Optional compatibility validator
+ /// Optional download service
+ public ExtensionService(
+ List availableExtensions,
+ Version? hostVersion = null,
+ Compatibility.ICompatibilityValidator? validator = null,
+ Download.ExtensionDownloadService? downloadService = null)
+ {
+ _availableExtensions = availableExtensions ?? throw new ArgumentNullException(nameof(availableExtensions));
+ _hostVersion = hostVersion;
+ _validator = validator;
+ _downloadService = downloadService;
+ }
+
+ ///
+ /// Updates the list of available extensions
+ ///
+ /// New list of available extensions
+ public void UpdateAvailableExtensions(List availableExtensions)
+ {
+ _availableExtensions = availableExtensions ?? throw new ArgumentNullException(nameof(availableExtensions));
+ }
+
+ ///
+ /// Queries available extensions based on filter criteria
+ ///
+ /// Query parameters including pagination and filters
+ /// Paginated result of extensions matching the query
+ public Task>> Query(ExtensionQueryDTO query)
+ {
+ try
+ {
+ if (query == null)
+ {
+ return Task.FromResult(HttpResponseDTO>.Failure(
+ "Query parameter cannot be null"));
+ }
+
+ // Validate pagination parameters
+ if (query.PageNumber < 1)
+ {
+ return Task.FromResult(HttpResponseDTO>.Failure(
+ "PageNumber must be greater than 0"));
+ }
+
+ if (query.PageSize < 1)
+ {
+ return Task.FromResult(HttpResponseDTO>.Failure(
+ "PageSize must be greater than 0"));
+ }
+
+ // Parse host version if provided
+ Version? queryHostVersion = null;
+ if (!string.IsNullOrWhiteSpace(query.HostVersion))
+ {
+ if (!Version.TryParse(query.HostVersion, out queryHostVersion))
+ {
+ return Task.FromResult(HttpResponseDTO>.Failure(
+ $"Invalid host version format: {query.HostVersion}"));
+ }
+ }
+
+ // Use query host version if provided, otherwise use service host version
+ var effectiveHostVersion = queryHostVersion ?? _hostVersion;
+
+ // Start with all available extensions
+ IEnumerable filtered = _availableExtensions;
+
+ // Apply filters
+ if (!string.IsNullOrWhiteSpace(query.Name))
+ {
+ filtered = filtered.Where(e =>
+ e.Descriptor.Name?.IndexOf(query.Name, StringComparison.OrdinalIgnoreCase) >= 0);
+ }
+
+ if (!string.IsNullOrWhiteSpace(query.Publisher))
+ {
+ filtered = filtered.Where(e =>
+ e.Descriptor.Publisher?.IndexOf(query.Publisher, StringComparison.OrdinalIgnoreCase) >= 0);
+ }
+
+ if (!string.IsNullOrWhiteSpace(query.Category))
+ {
+ filtered = filtered.Where(e =>
+ e.Descriptor.Categories?.Any(c =>
+ c.IndexOf(query.Category, StringComparison.OrdinalIgnoreCase) >= 0) == true);
+ }
+
+ if (query.TargetPlatform.HasValue && query.TargetPlatform.Value != TargetPlatform.None)
+ {
+ filtered = filtered.Where(e =>
+ (e.Descriptor.SupportedPlatforms & query.TargetPlatform.Value) != 0);
+ }
+
+ if (!query.IncludePreRelease)
+ {
+ filtered = filtered.Where(e => !e.IsPreRelease);
+ }
+
+ if (!string.IsNullOrWhiteSpace(query.SearchTerm))
+ {
+ filtered = filtered.Where(e =>
+ (e.Descriptor.Name?.IndexOf(query.SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0) ||
+ (e.Descriptor.DisplayName?.IndexOf(query.SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0) ||
+ (e.Descriptor.Description?.IndexOf(query.SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0));
+ }
+
+ // Convert to list for pagination
+ var filteredList = filtered.ToList();
+
+ // Calculate pagination
+ var totalCount = filteredList.Count;
+ var totalPages = (int)Math.Ceiling(totalCount / (double)query.PageSize);
+
+ // Apply pagination
+ var items = filteredList
+ .Skip((query.PageNumber - 1) * query.PageSize)
+ .Take(query.PageSize)
+ .Select(e => MapToExtensionDTO(e, effectiveHostVersion))
+ .ToList();
+
+ var result = new PagedResultDTO
+ {
+ PageNumber = query.PageNumber,
+ PageSize = query.PageSize,
+ TotalCount = totalCount,
+ TotalPages = totalPages,
+ Items = items
+ };
+
+ return Task.FromResult(HttpResponseDTO>.Success(result));
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult(HttpResponseDTO>.InnerException(
+ $"Error querying extensions: {ex.Message}"));
+ }
+ }
+
+ ///
+ /// Downloads an extension and its dependencies by ID
+ ///
+ /// Extension ID (Name)
+ /// Download result containing file name and stream
+ public async Task> Download(string id)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ return HttpResponseDTO.Failure("Extension ID cannot be null or empty");
+ }
+
+ if (_downloadService == null)
+ {
+ return HttpResponseDTO.Failure(
+ "Download service is not configured");
+ }
+
+ // Find the extension by ID (using Name as ID)
+ var extension = _availableExtensions.FirstOrDefault(e =>
+ e.Descriptor.Name?.Equals(id, StringComparison.OrdinalIgnoreCase) == true);
+
+ if (extension == null)
+ {
+ return HttpResponseDTO.Failure(
+ $"Extension with ID '{id}' not found");
+ }
+
+ // Collect all extensions to download (main extension + dependencies)
+ var extensionsToDownload = new List { extension };
+
+ // Resolve dependencies
+ if (extension.Descriptor.Dependencies != null && extension.Descriptor.Dependencies.Count > 0)
+ {
+ foreach (var depId in extension.Descriptor.Dependencies)
+ {
+ var dependency = _availableExtensions.FirstOrDefault(e =>
+ e.Descriptor.Name?.Equals(depId, StringComparison.OrdinalIgnoreCase) == true);
+
+ if (dependency != null)
+ {
+ extensionsToDownload.Add(dependency);
+ }
+ }
+ }
+
+ // For now, we'll download only the main extension
+ // In a real implementation, you might want to download all dependencies
+ // and package them together or return multiple files
+
+ var updateQueue = new Download.UpdateQueue();
+ var operation = updateQueue.Enqueue(extension, false);
+
+ var downloadedPath = await _downloadService.DownloadAsync(operation);
+
+ if (downloadedPath == null || !File.Exists(downloadedPath))
+ {
+ return HttpResponseDTO.Failure(
+ $"Failed to download extension '{extension.Descriptor.DisplayName}'");
+ }
+
+ // Read the file into a memory stream
+ var fileBytes = File.ReadAllBytes(downloadedPath);
+ var stream = new MemoryStream(fileBytes);
+
+ var result = new DownloadExtensionDTO
+ {
+ FileName = Path.GetFileName(downloadedPath),
+ Stream = stream
+ };
+
+ return HttpResponseDTO.Success(result);
+ }
+ catch (Exception ex)
+ {
+ return HttpResponseDTO.InnerException(
+ $"Error downloading extension: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Maps an AvailableExtension to an ExtensionDTO
+ ///
+ private ExtensionDTO MapToExtensionDTO(AvailableExtension extension, Version? hostVersion)
+ {
+ var descriptor = extension.Descriptor;
+
+ // Determine compatibility if host version is provided
+ bool? isCompatible = null;
+ if (hostVersion != null && _validator != null)
+ {
+ isCompatible = _validator.IsCompatible(descriptor);
+ }
+
+ return new ExtensionDTO
+ {
+ Id = descriptor.Name ?? string.Empty,
+ Name = descriptor.Name,
+ DisplayName = descriptor.DisplayName,
+ Version = descriptor.Version,
+ FileSize = descriptor.PackageSize > 0 ? descriptor.PackageSize : (long?)null,
+ UploadTime = descriptor.ReleaseDate,
+ Status = true, // Assume enabled if it's in the available list
+ Description = descriptor.Description,
+ Format = GetFileFormat(descriptor.DownloadUrl),
+ Hash = descriptor.PackageHash,
+ Publisher = descriptor.Publisher,
+ License = descriptor.License,
+ Categories = descriptor.Categories,
+ SupportedPlatforms = descriptor.SupportedPlatforms,
+ MinHostVersion = descriptor.Compatibility?.MinHostVersion?.ToString(),
+ MaxHostVersion = descriptor.Compatibility?.MaxHostVersion?.ToString(),
+ ReleaseDate = descriptor.ReleaseDate,
+ Dependencies = descriptor.Dependencies,
+ IsPreRelease = extension.IsPreRelease,
+ DownloadUrl = descriptor.DownloadUrl,
+ CustomProperties = descriptor.CustomProperties,
+ IsCompatible = isCompatible
+ };
+ }
+
+ ///
+ /// Extracts file format from download URL
+ ///
+ private string? GetFileFormat(string? downloadUrl)
+ {
+ if (string.IsNullOrWhiteSpace(downloadUrl))
+ return null;
+
+ try
+ {
+ var extension = Path.GetExtension(downloadUrl);
+ return string.IsNullOrWhiteSpace(extension) ? null : extension;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs
new file mode 100644
index 00000000..b89bf1ab
--- /dev/null
+++ b/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using GeneralUpdate.Extension.DTOs;
+
+namespace GeneralUpdate.Extension.Services
+{
+ ///
+ /// Interface for extension query and download operations
+ ///
+ public interface IExtensionService
+ {
+ ///
+ /// Updates the list of available extensions
+ ///
+ /// New list of available extensions
+ void UpdateAvailableExtensions(List availableExtensions);
+
+ ///
+ /// Queries available extensions based on filter criteria
+ ///
+ /// Query parameters including pagination and filters
+ /// Paginated result of extensions matching the query
+ Task>> Query(ExtensionQueryDTO query);
+
+ ///
+ /// Downloads an extension and its dependencies by ID
+ ///
+ /// Extension ID
+ /// Download result containing file name and stream
+ Task> Download(string id);
+ }
+}
From 32e5aa659671ef8bb63d877b553c7cc462f52414 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 09:26:09 +0000
Subject: [PATCH 3/9] Add unit tests for DTOs and ExtensionService
Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com>
---
.../DTOs/HttpResponseDTOTests.cs | 71 +++
.../ExtensionTest/DTOs/PagedResultDTOTests.cs | 92 ++++
.../Services/ExtensionServiceTests.cs | 416 ++++++++++++++++++
3 files changed, 579 insertions(+)
create mode 100644 src/c#/ExtensionTest/DTOs/HttpResponseDTOTests.cs
create mode 100644 src/c#/ExtensionTest/DTOs/PagedResultDTOTests.cs
create mode 100644 src/c#/ExtensionTest/Services/ExtensionServiceTests.cs
diff --git a/src/c#/ExtensionTest/DTOs/HttpResponseDTOTests.cs b/src/c#/ExtensionTest/DTOs/HttpResponseDTOTests.cs
new file mode 100644
index 00000000..40976e58
--- /dev/null
+++ b/src/c#/ExtensionTest/DTOs/HttpResponseDTOTests.cs
@@ -0,0 +1,71 @@
+using GeneralUpdate.Extension.DTOs;
+using Xunit;
+
+namespace ExtensionTest.DTOs
+{
+ ///
+ /// Contains test cases for HttpResponseDTO classes
+ ///
+ public class HttpResponseDTOTests
+ {
+ [Fact]
+ public void HttpResponseDTO_Success_ShouldCreateSuccessResponse()
+ {
+ // Act
+ var response = HttpResponseDTO.Success("Operation successful");
+
+ // Assert
+ Assert.Equal(200, response.Code);
+ Assert.Equal("Operation successful", response.Message);
+ }
+
+ [Fact]
+ public void HttpResponseDTO_InnerException_ShouldCreateErrorResponse()
+ {
+ // Act
+ var response = HttpResponseDTO.InnerException("An error occurred");
+
+ // Assert
+ Assert.Equal(500, response.Code);
+ Assert.Equal("An error occurred", response.Message);
+ }
+
+ [Fact]
+ public void HttpResponseDTOGeneric_Success_ShouldCreateSuccessResponseWithData()
+ {
+ // Arrange
+ var testData = new { Id = 1, Name = "Test" };
+
+ // Act
+ var response = HttpResponseDTO