Skip to content
2 changes: 1 addition & 1 deletion src/c#/ExtensionTest/Services/ExtensionServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private List<AvailableExtension> CreateTestExtensions()
private ExtensionService CreateExtensionService(List<AvailableExtension> extensions)
{
var updateQueue = new GeneralUpdate.Extension.Download.UpdateQueue();
return new ExtensionService(extensions, "/tmp/test-downloads", updateQueue);
return new ExtensionService(extensions, "/tmp/test-downloads", updateQueue, "https://test-server.com/api/extensions");
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit tests are now broken by the refactoring. The Query method now makes actual HTTP requests to the server instead of querying in-memory data. These tests will fail at runtime because:

  1. They don't set up any HTTP mocking (no HttpClient mocking with HttpMessageHandler)
  2. They expect the Query method to filter the in-memory extensions list, but it now makes HTTP requests
  3. The test server URL "https://test-server.com/api/extensions" won't respond

As noted in the PR description's Breaking Changes section, tests need to be updated to mock HTTP requests. Consider using a mocking library like RichardSzalay.MockHttp or creating a test HttpMessageHandler to mock HTTP responses.

Copilot uses AI. Check for mistakes.
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public void Initialize()
var hostVersion = new Version(1, 5, 0);
var installPath = @"C:\MyApp\Extensions";
var downloadPath = @"C:\MyApp\Temp\Downloads";
var serverUrl = "https://your-server.com/api/extensions";

// Detect current platform
var currentPlatform = DetectCurrentPlatform();
Expand All @@ -31,6 +32,7 @@ public void Initialize()
HostVersion = hostVersion,
InstallBasePath = installPath,
DownloadPath = downloadPath,
ServerUrl = serverUrl,
TargetPlatform = currentPlatform,
DownloadTimeout = 300 // 5 minutes
};
Expand Down
9 changes: 9 additions & 0 deletions src/c#/GeneralUpdate.Extension/ExtensionHostConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ public class ExtensionHostConfig
/// </summary>
public string DownloadPath { get; set; } = null!;

/// <summary>
/// Gets or sets the server URL for extension queries and downloads.
/// This is the base URL used to construct Query and Download endpoints.
/// Example: "https://your-server.com/api/extensions"
/// </summary>
public string ServerUrl { get; set; } = null!;

/// <summary>
/// Gets or sets the target platform (Windows/Linux/macOS).
/// Defaults to Windows if not specified.
Expand Down Expand Up @@ -62,6 +69,8 @@ public void Validate()
throw new ArgumentNullException(nameof(InstallBasePath));
if (string.IsNullOrWhiteSpace(DownloadPath))
throw new ArgumentNullException(nameof(DownloadPath));
if (string.IsNullOrWhiteSpace(ServerUrl))
throw new ArgumentNullException(nameof(ServerUrl));
}
}
}
4 changes: 4 additions & 0 deletions src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public GeneralExtensionHost(ExtensionHostConfig config)
new List<Metadata.AvailableExtension>(),
config.DownloadPath,
_updateQueue,
config.ServerUrl,
config.HostVersion,
_validator,
config.DownloadTimeout,
Expand All @@ -130,6 +131,7 @@ public GeneralExtensionHost(ExtensionHostConfig config)
/// <param name="hostVersion">The current host application version.</param>
/// <param name="installBasePath">Base directory for extension installations.</param>
/// <param name="downloadPath">Directory for downloading extension packages.</param>
/// <param name="serverUrl">Server base URL for extension queries and downloads.</param>
/// <param name="targetPlatform">The current platform (Windows/Linux/macOS).</param>
/// <param name="downloadTimeout">Download timeout in seconds (default: 300).</param>
/// <param name="authScheme">Optional HTTP authentication scheme (e.g., "Bearer", "Basic").</param>
Expand All @@ -140,6 +142,7 @@ public GeneralExtensionHost(
Version hostVersion,
string installBasePath,
string downloadPath,
string serverUrl,
Metadata.TargetPlatform targetPlatform = Metadata.TargetPlatform.Windows,
int downloadTimeout = 300,
string? authScheme = null,
Expand All @@ -149,6 +152,7 @@ public GeneralExtensionHost(
HostVersion = hostVersion,
InstallBasePath = installBasePath,
DownloadPath = downloadPath,
ServerUrl = serverUrl,
TargetPlatform = targetPlatform,
DownloadTimeout = downloadTimeout,
AuthScheme = authScheme,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ public class ExtensionDescriptor

/// <summary>
/// Gets or sets the download URL for the extension package.
/// NOTE: This field is optional and primarily for backward compatibility.
/// The system constructs download URLs dynamically from the ServerUrl configured in ExtensionHostConfig.
/// Format: {ServerUrl}/Download/{ExtensionName}
/// </summary>
[JsonPropertyName("downloadUrl")]
public string? DownloadUrl { get; set; }
Expand Down
94 changes: 69 additions & 25 deletions src/c#/GeneralUpdate.Extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,17 @@ Note: This library is currently distributed as source. A NuGet package may be av
using GeneralUpdate.Extension;
using GeneralUpdate.Extension.Metadata;

// Create extension host
var host = new ExtensionHost(
hostVersion: new Version(1, 0, 0),
installPath: @"C:\MyApp\Extensions",
downloadPath: @"C:\MyApp\Downloads",
targetPlatform: TargetPlatform.Windows);
// Create extension host with configuration
var config = new ExtensionHostConfig
{
HostVersion = new Version(1, 0, 0),
InstallBasePath = @"C:\MyApp\Extensions",
DownloadPath = @"C:\MyApp\Downloads",
ServerUrl = "https://your-server.com/api/extensions",
TargetPlatform = TargetPlatform.Windows
};

var host = new GeneralExtensionHost(config);

// Load installed extensions
host.LoadInstalledExtensions();
Expand All @@ -54,6 +59,32 @@ host.UpdateStateChanged += (sender, args) =>
var installed = host.GetInstalledExtensions();
```

## Server URL Architecture

The extension system uses a server-based architecture for querying and downloading extensions. The `ServerUrl` configured in `ExtensionHostConfig` serves as the base URL for all extension operations.

### URL Construction

The system automatically constructs the following endpoints:

- **Query Endpoint**: `{ServerUrl}/Query` - Used for searching and filtering extensions
- **Download Endpoint**: `{ServerUrl}/Download/{ExtensionName}` - Used for downloading extension packages

### Example

If your `ServerUrl` is `https://your-server.com/api/extensions`:
- Query endpoint: `https://your-server.com/api/extensions/Query`
- Download for extension "my-extension": `https://your-server.com/api/extensions/Download/my-extension`

### Server Requirements

Your server should implement these endpoints:

1. **Query Endpoint** - Returns available extensions based on filter criteria
2. **Download Endpoint** - Returns the extension package file (typically .zip format)

The `DownloadUrl` field in extension descriptors is now optional and primarily for backward compatibility. The system constructs download URLs dynamically from the configured ServerUrl.

## Complete Usage Guide

### 1. Dependency Injection Setup
Expand All @@ -70,25 +101,29 @@ public class YourModule : IModule
{
public void RegisterTypes(IContainerRegistry containerRegistry)
{
var hostVersion = new Version(1, 0, 0);
var installPath = @"C:\MyApp\Extensions";
var downloadPath = @"C:\MyApp\Downloads";
var platform = Metadata.TargetPlatform.Windows;
var config = new ExtensionHostConfig
{
HostVersion = new Version(1, 0, 0),
InstallBasePath = @"C:\MyApp\Extensions",
DownloadPath = @"C:\MyApp\Downloads",
ServerUrl = "https://your-server.com/api/extensions",
TargetPlatform = Metadata.TargetPlatform.Windows
};

// Register as singletons
containerRegistry.RegisterSingleton<Core.IExtensionCatalog>(() =>
new Core.ExtensionCatalog(installPath));
new Core.ExtensionCatalog(config.InstallBasePath));

containerRegistry.RegisterSingleton<Compatibility.ICompatibilityValidator>(() =>
new Compatibility.CompatibilityValidator(hostVersion));
new Compatibility.CompatibilityValidator(config.HostVersion));

containerRegistry.RegisterSingleton<Download.IUpdateQueue, Download.UpdateQueue>();

containerRegistry.RegisterSingleton<PackageGeneration.IExtensionPackageGenerator,
PackageGeneration.ExtensionPackageGenerator>();

containerRegistry.RegisterSingleton<IExtensionHost>(() =>
new ExtensionHost(hostVersion, installPath, downloadPath, platform));
new GeneralExtensionHost(config));
}
}

Expand All @@ -102,24 +137,28 @@ var host = container.Resolve<IExtensionHost>();
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
var hostVersion = new Version(1, 0, 0);
var installPath = @"C:\Extensions";
var downloadPath = @"C:\Downloads";
var config = new ExtensionHostConfig
{
HostVersion = new Version(1, 0, 0),
InstallBasePath = @"C:\Extensions",
DownloadPath = @"C:\Downloads",
ServerUrl = "https://your-server.com/api/extensions",
TargetPlatform = Metadata.TargetPlatform.Windows
};

services.AddSingleton<Core.IExtensionCatalog>(sp =>
new Core.ExtensionCatalog(installPath));
new Core.ExtensionCatalog(config.InstallBasePath));

services.AddSingleton<Compatibility.ICompatibilityValidator>(sp =>
new Compatibility.CompatibilityValidator(hostVersion));
new Compatibility.CompatibilityValidator(config.HostVersion));

services.AddSingleton<Download.IUpdateQueue, Download.UpdateQueue>();

services.AddSingleton<PackageGeneration.IExtensionPackageGenerator,
PackageGeneration.ExtensionPackageGenerator>();

services.AddSingleton<IExtensionHost>(sp =>
new ExtensionHost(hostVersion, installPath, downloadPath,
Metadata.TargetPlatform.Windows));
new GeneralExtensionHost(config));

var provider = services.BuildServiceProvider();
var host = provider.GetRequiredService<IExtensionHost>();
Expand All @@ -128,11 +167,16 @@ var host = provider.GetRequiredService<IExtensionHost>();
#### Without DI (Direct Instantiation)

```csharp
var host = new ExtensionHost(
new Version(1, 0, 0),
@"C:\Extensions",
@"C:\Downloads",
Metadata.TargetPlatform.Windows);
var config = new ExtensionHostConfig
{
HostVersion = new Version(1, 0, 0),
InstallBasePath = @"C:\Extensions",
DownloadPath = @"C:\Downloads",
ServerUrl = "https://your-server.com/api/extensions",
TargetPlatform = Metadata.TargetPlatform.Windows
};

var host = new GeneralExtensionHost(config);
```

### 2. Loading and Managing Extensions
Expand Down
Loading
Loading