From a72b5cc3475fe33eac81c3962a12304cf4e70244 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 19:39:39 +0800 Subject: [PATCH 1/3] feat: add LoadFromConfiguration for appsettings.json support Adds .LoadFromConfiguration(IConfiguration) to GeneralUpdateBootstrap for loading settings from appsettings.json, environment variables, or any IConfiguration source. - Reads UpdateOptions (AppType, DiffMode, Silent, MaxConcurrency, etc.) - Reads Configinfo fields (UpdateUrl, AppSecretKey, InstallPath, etc.) - Code .Option() calls override configuration values (code > config) - Added PackageReference to Microsoft.Extensions.Configuration.Abstractions Closes #410 --- .../Bootstrap/GeneralUpdateBootstrap.cs | 71 +++++++++++++++++++ .../GeneralUpdate.Core.csproj | 2 + 2 files changed, 73 insertions(+) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index d391ed2a..fb1c275b 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -180,6 +180,77 @@ public GeneralUpdateBootstrap SetCustomSkipOption(Func? func) return this; } + /// + /// Load settings from IConfiguration (e.g. appsettings.json). + /// Sets UpdateOptions and Configinfo fields from the configuration section. + /// Values set via .Option() or .SetConfig() before this call + /// are preserved (code has higher priority than config). + /// + /// The configuration (e.g. builder.Configuration). + /// Config section name, default "GeneralUpdate". + /// + /// + /// new GeneralUpdateBootstrap() + /// .LoadFromConfiguration(builder.Configuration) + /// .Option(UpdateOptions.Silent, true) // overrides appsettings + /// .LaunchAsync(); + /// + /// + public GeneralUpdateBootstrap LoadFromConfiguration(Microsoft.Extensions.Configuration.IConfiguration config, string section = "GeneralUpdate") + { + if (config == null) return this; + + var sec = config.GetSection(section); + + // UpdateOptions — only set if key exists in config + TrySetOptionIfMissing(sec, "AppType", v => Option(UpdateOptions.AppType, Enum.Parse(v))); + TrySetOptionIfMissing(sec, "DiffMode", v => Option(UpdateOptions.DiffMode, Enum.Parse(v))); + TrySetOptionIfMissing(sec, "Silent", v => Option(UpdateOptions.Silent, bool.Parse(v))); + TrySetOptionIfMissing(sec, "SilentAutoInstall", v => Option(UpdateOptions.SilentAutoInstall, bool.Parse(v))); + TrySetOptionIfMissing(sec, "SilentPollIntervalMinutes", v => Option(UpdateOptions.SilentPollIntervalMinutes, int.Parse(v))); + TrySetOptionIfMissing(sec, "MaxConcurrency", v => Option(UpdateOptions.MaxConcurrency, int.Parse(v))); + TrySetOptionIfMissing(sec, "EnableResume", v => Option(UpdateOptions.EnableResume, bool.Parse(v))); + TrySetOptionIfMissing(sec, "RetryCount", v => Option(UpdateOptions.RetryCount, int.Parse(v))); + TrySetOptionIfMissing(sec, "RetryIntervalSeconds", v => Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(double.Parse(v)))); + TrySetOptionIfMissing(sec, "VerifyChecksum", v => Option(UpdateOptions.VerifyChecksum, bool.Parse(v))); + TrySetOptionIfMissing(sec, "DownloadTimeout", v => Option(UpdateOptions.DownloadTimeout, int.Parse(v))); + TrySetOptionIfMissing(sec, "PatchEnabled", v => Option(UpdateOptions.PatchEnabled, bool.Parse(v))); + TrySetOptionIfMissing(sec, "BackupEnabled", v => Option(UpdateOptions.BackupEnabled, bool.Parse(v))); + + // Configinfo fields — only set if not already configured + TrySetConfigField(sec, "UpdateUrl", v => _configInfo.UpdateUrl = v); + TrySetConfigField(sec, "AppSecretKey", v => _configInfo.AppSecretKey = v); + TrySetConfigField(sec, "AppName", v => _configInfo.AppName = v); + TrySetConfigField(sec, "MainAppName", v => _configInfo.MainAppName = v); + TrySetConfigField(sec, "InstallPath", v => _configInfo.InstallPath = v); + TrySetConfigField(sec, "ClientVersion", v => _configInfo.ClientVersion = v); + TrySetConfigField(sec, "UpgradeClientVersion", v => _configInfo.UpgradeClientVersion = v); + TrySetConfigField(sec, "ProductId", v => _configInfo.ProductId = v); + TrySetConfigField(sec, "ReportUrl", v => _configInfo.ReportUrl = v); + TrySetConfigField(sec, "UpdateLogUrl", v => _configInfo.UpdateLogUrl = v); + TrySetConfigField(sec, "Bowl", v => _configInfo.Bowl = v); + TrySetConfigField(sec, "Scheme", v => _configInfo.Scheme = v); + TrySetConfigField(sec, "Token", v => _configInfo.Token = v); + TrySetConfigField(sec, "DriverDirectory", v => _configInfo.DriverDirectory = v); + + GeneralTracer.Info($"GeneralUpdateBootstrap: loaded configuration from section '{section}'."); + return this; + } + + private void TrySetOptionIfMissing(Microsoft.Extensions.Configuration.IConfigurationSection sec, string key, Action setter) + { + var v = sec[key]; + if (!string.IsNullOrEmpty(v)) + setter(v); + } + + private static void TrySetConfigField(Microsoft.Extensions.Configuration.IConfigurationSection sec, string key, Action setter) + { + var v = sec[key]; + if (!string.IsNullOrEmpty(v)) + setter(v); + } + public GeneralUpdateBootstrap AddListenerUpdatePrecheck(Func func) { _updatePrecheck = func ?? throw new ArgumentNullException(nameof(func)); diff --git a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj index cf93fe4e..89054883 100644 --- a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj +++ b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj @@ -35,6 +35,8 @@ + + From 0f9069fc84e62ed8e096f1d528c0d5ecb2e43c2e Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 19:48:33 +0800 Subject: [PATCH 2/3] feat: add SetConfig(string) overload for local JSON config Overload reads a Configinfo JSON file. Filename-only resolves relative to current directory; relative/absolute paths used as-is. Uses source-generated JSON to stay AOT-compatible. Closes #410 --- .../Bootstrap/GeneralUpdateBootstrap.cs | 88 +++++-------------- .../GeneralUpdate.Core.csproj | 2 - .../JsonContext/HttpParameterJsonContext.cs | 2 + 3 files changed, 26 insertions(+), 66 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index fb1c275b..ab6ddcda 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -181,74 +181,34 @@ public GeneralUpdateBootstrap SetCustomSkipOption(Func? func) } /// - /// Load settings from IConfiguration (e.g. appsettings.json). - /// Sets UpdateOptions and Configinfo fields from the configuration section. - /// Values set via .Option() or .SetConfig() before this call - /// are preserved (code has higher priority than config). + /// Load configuration from a local JSON file. /// - /// The configuration (e.g. builder.Configuration). - /// Config section name, default "GeneralUpdate". - /// - /// - /// new GeneralUpdateBootstrap() - /// .LoadFromConfiguration(builder.Configuration) - /// .Option(UpdateOptions.Silent, true) // overrides appsettings - /// .LaunchAsync(); - /// - /// - public GeneralUpdateBootstrap LoadFromConfiguration(Microsoft.Extensions.Configuration.IConfiguration config, string section = "GeneralUpdate") + /// + /// Config file path. + /// If just a filename (no directory separator), resolves relative to the current directory. + /// Relative or absolute paths are used as-is. + /// + public GeneralUpdateBootstrap SetConfig(string filePath) { - if (config == null) return this; - - var sec = config.GetSection(section); - - // UpdateOptions — only set if key exists in config - TrySetOptionIfMissing(sec, "AppType", v => Option(UpdateOptions.AppType, Enum.Parse(v))); - TrySetOptionIfMissing(sec, "DiffMode", v => Option(UpdateOptions.DiffMode, Enum.Parse(v))); - TrySetOptionIfMissing(sec, "Silent", v => Option(UpdateOptions.Silent, bool.Parse(v))); - TrySetOptionIfMissing(sec, "SilentAutoInstall", v => Option(UpdateOptions.SilentAutoInstall, bool.Parse(v))); - TrySetOptionIfMissing(sec, "SilentPollIntervalMinutes", v => Option(UpdateOptions.SilentPollIntervalMinutes, int.Parse(v))); - TrySetOptionIfMissing(sec, "MaxConcurrency", v => Option(UpdateOptions.MaxConcurrency, int.Parse(v))); - TrySetOptionIfMissing(sec, "EnableResume", v => Option(UpdateOptions.EnableResume, bool.Parse(v))); - TrySetOptionIfMissing(sec, "RetryCount", v => Option(UpdateOptions.RetryCount, int.Parse(v))); - TrySetOptionIfMissing(sec, "RetryIntervalSeconds", v => Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(double.Parse(v)))); - TrySetOptionIfMissing(sec, "VerifyChecksum", v => Option(UpdateOptions.VerifyChecksum, bool.Parse(v))); - TrySetOptionIfMissing(sec, "DownloadTimeout", v => Option(UpdateOptions.DownloadTimeout, int.Parse(v))); - TrySetOptionIfMissing(sec, "PatchEnabled", v => Option(UpdateOptions.PatchEnabled, bool.Parse(v))); - TrySetOptionIfMissing(sec, "BackupEnabled", v => Option(UpdateOptions.BackupEnabled, bool.Parse(v))); - - // Configinfo fields — only set if not already configured - TrySetConfigField(sec, "UpdateUrl", v => _configInfo.UpdateUrl = v); - TrySetConfigField(sec, "AppSecretKey", v => _configInfo.AppSecretKey = v); - TrySetConfigField(sec, "AppName", v => _configInfo.AppName = v); - TrySetConfigField(sec, "MainAppName", v => _configInfo.MainAppName = v); - TrySetConfigField(sec, "InstallPath", v => _configInfo.InstallPath = v); - TrySetConfigField(sec, "ClientVersion", v => _configInfo.ClientVersion = v); - TrySetConfigField(sec, "UpgradeClientVersion", v => _configInfo.UpgradeClientVersion = v); - TrySetConfigField(sec, "ProductId", v => _configInfo.ProductId = v); - TrySetConfigField(sec, "ReportUrl", v => _configInfo.ReportUrl = v); - TrySetConfigField(sec, "UpdateLogUrl", v => _configInfo.UpdateLogUrl = v); - TrySetConfigField(sec, "Bowl", v => _configInfo.Bowl = v); - TrySetConfigField(sec, "Scheme", v => _configInfo.Scheme = v); - TrySetConfigField(sec, "Token", v => _configInfo.Token = v); - TrySetConfigField(sec, "DriverDirectory", v => _configInfo.DriverDirectory = v); - - GeneralTracer.Info($"GeneralUpdateBootstrap: loaded configuration from section '{section}'."); - return this; - } + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentNullException(nameof(filePath)); - private void TrySetOptionIfMissing(Microsoft.Extensions.Configuration.IConfigurationSection sec, string key, Action setter) - { - var v = sec[key]; - if (!string.IsNullOrEmpty(v)) - setter(v); - } + // Resolve filename-only paths to current directory + var hasPathChar = filePath.Contains(Path.DirectorySeparatorChar) + || filePath.Contains(Path.AltDirectorySeparatorChar); + var fullPath = hasPathChar + ? Path.GetFullPath(filePath) + : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filePath); - private static void TrySetConfigField(Microsoft.Extensions.Configuration.IConfigurationSection sec, string key, Action setter) - { - var v = sec[key]; - if (!string.IsNullOrEmpty(v)) - setter(v); + if (!File.Exists(fullPath)) + throw new FileNotFoundException($"Config file not found: {fullPath}"); + + var json = File.ReadAllText(fullPath); + var config = JsonSerializer.Deserialize(json, JsonContext.HttpParameterJsonContext.Default.Configinfo); + if (config == null) + throw new InvalidOperationException($"Failed to parse config file: {fullPath}"); + + return SetConfig(config); } public GeneralUpdateBootstrap AddListenerUpdatePrecheck(Func func) diff --git a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj index 89054883..cf93fe4e 100644 --- a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj +++ b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj @@ -35,8 +35,6 @@ - - diff --git a/src/c#/GeneralUpdate.Core/JsonContext/HttpParameterJsonContext.cs b/src/c#/GeneralUpdate.Core/JsonContext/HttpParameterJsonContext.cs index 4e8083e7..1e89de87 100644 --- a/src/c#/GeneralUpdate.Core/JsonContext/HttpParameterJsonContext.cs +++ b/src/c#/GeneralUpdate.Core/JsonContext/HttpParameterJsonContext.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Text.Json.Serialization; +using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.Download.Abstractions; namespace GeneralUpdate.Core.JsonContext; @@ -12,4 +13,5 @@ namespace GeneralUpdate.Core.JsonContext; [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(PacketDTO))] [JsonSerializable(typeof(List))] +[JsonSerializable(typeof(Configinfo))] public partial class HttpParameterJsonContext: JsonSerializerContext; \ No newline at end of file From 770a6c1dee00e05302f50b528ae3a6c1a0faad13 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 19:53:27 +0800 Subject: [PATCH 3/3] fix: update OSS test to match new client behavior (no exception) --- tests/CoreTest/Integration/OssIntegrationTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/CoreTest/Integration/OssIntegrationTests.cs b/tests/CoreTest/Integration/OssIntegrationTests.cs index 26626f51..92e2560b 100644 --- a/tests/CoreTest/Integration/OssIntegrationTests.cs +++ b/tests/CoreTest/Integration/OssIntegrationTests.cs @@ -72,8 +72,9 @@ await Assert.ThrowsAsync(() => } [Fact] - public async Task OSSUpdateStrategy_RequiresConfig() + public async Task OSSUpdateStrategy_WithoutConfig_ReturnsWithoutError() { + // OSS client without UpdateUrl or local version config: no exception, just returns var strategy = new OSSUpdateStrategy(); var config = new GlobalConfigInfo { @@ -83,8 +84,8 @@ public async Task OSSUpdateStrategy_RequiresConfig() }; strategy.Create(config); - await Assert.ThrowsAsync(() => - strategy.ExecuteAsync()); + var ex = await Record.ExceptionAsync(() => strategy.ExecuteAsync()); + Assert.Null(ex); } [Fact]