diff --git a/Modules/Hosting/ApplicationHost.cs b/Modules/Hosting/ApplicationHost.cs
index 25c2afa..e662f7e 100644
--- a/Modules/Hosting/ApplicationHost.cs
+++ b/Modules/Hosting/ApplicationHost.cs
@@ -29,6 +29,12 @@ public class ApplicationHost
private string _apiName = null!;
private string _apiVersion = null!;
+ ///
+ /// 插件加载器实例,持有所有插件 AssemblyLoadContext 的强引用,
+ /// 确保在服务注册期间 ALC 不会被 GC 提前标记为 unloading。
+ ///
+ private PluginLoader _pluginLoader = null!;
+
///
/// 初始化应用程序主机
///
@@ -65,8 +71,8 @@ public ApplicationHost Build()
// 6. 加载插件
var pluginLogger = new SerilogLoggerFactory(Log.Logger).CreateLogger("PluginLoader");
- var pluginLoader = new PluginLoader(_configuration, pluginLogger);
- var plugins = pluginLoader.LoadPlugins();
+ _pluginLoader = new PluginLoader(_configuration, pluginLogger);
+ var plugins = _pluginLoader.LoadPlugins();
// 7. 检查依赖
var dependencyChecker = new PluginDependencyChecker(pluginLogger);
@@ -75,7 +81,7 @@ public ApplicationHost Build()
// 8. 注册服务
// 将插件集合注入到 DI 容器(作为单例),插件实现可从容器中获取此集合
builder.Services.AddSingleton(plugins);
-
+
var serviceRegistrar = new PluginServiceRegistrar(builder.Services, pluginLogger);
serviceRegistrar.AddSwaggerServices(_apiName, _apiVersion);
serviceRegistrar.RegisterPluginServices(plugins, _configuration);
diff --git a/Modules/PluginManagement/PluginLoader.cs b/Modules/PluginManagement/PluginLoader.cs
index 5edcc85..de296f3 100644
--- a/Modules/PluginManagement/PluginLoader.cs
+++ b/Modules/PluginManagement/PluginLoader.cs
@@ -14,6 +14,17 @@ public class PluginLoader
private readonly IConfiguration _configuration;
private readonly ILogger _logger;
+ ///
+ /// 跟踪所有已创建的 AssemblyLoadContext,确保在插件服务注册期间 ALC 保持 alive 状态,
+ /// 防止因失去强引用而被 GC 提前标记为 unloading。
+ ///
+ private readonly List _loadContexts = new();
+
+ ///
+ /// 获取所有已创建的插件加载上下文列表。
+ ///
+ public IReadOnlyList LoadContexts => _loadContexts.AsReadOnly();
+
///
/// 初始化插件加载器
///
@@ -32,7 +43,7 @@ public PluginLoader(IConfiguration configuration, ILogger logger)
public List LoadPlugins()
{
var loadedPlugins = new List();
-
+
// 获取插件目录路径
string pluginsPath = Path.Combine(AppContext.BaseDirectory, "plugins");
@@ -221,8 +232,13 @@ private List LoadPluginFromDirectory(string dirPath)
{
// 使用隔离加载上下文加载程序集,防止插件之间的依赖冲突
var loadContext = new PluginLoadContext(dllPath);
+
+ // 将 ALC 添加到跟踪列表,确保其在服务注册阶段保持 alive,
+ // 不会被 GC 标记为 unloading(isCollectible: true 的 ALC 若无强引用会被回收)
+ _loadContexts.Add(loadContext);
+
var assembly = loadContext.LoadFromAssemblyPath(dllPath);
-
+
// 从程序集中查找所有实现了 IApiPlugin 接口的类型
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(IApiPlugin).IsAssignableFrom(t) && !t.IsInterface);
@@ -238,4 +254,24 @@ private List LoadPluginFromDirectory(string dirPath)
return null;
}
+
+ ///
+ /// 卸载所有插件 AssemblyLoadContext,释放占用的资源。
+ /// 应在完成所有需要 ALC 的操作(如服务注册)后调用。
+ ///
+ public void UnloadAll()
+ {
+ foreach (var ctx in _loadContexts)
+ {
+ try
+ {
+ ctx.Unload();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to unload plugin assembly load context");
+ }
+ }
+ _loadContexts.Clear();
+ }
}
diff --git a/Modules/Services/ServiceRegistration.cs b/Modules/Services/ServiceRegistration.cs
index 6f41f46..46ecc7a 100644
--- a/Modules/Services/ServiceRegistration.cs
+++ b/Modules/Services/ServiceRegistration.cs
@@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using System.Text.Json.Nodes;
using sharwapi.Contracts.Core;
+using System.IO;
namespace sharwapi.Core.Modules.Services;
@@ -47,7 +49,7 @@ private void RegisterPluginServices(IApiPlugin plugin, IConfiguration configurat
{
// 构建插件配置文件的完整路径
var configPath = Path.Combine(AppContext.BaseDirectory, "config", $"{plugin.Name}.json");
-
+
// 确保配置文件存在,如果不存在则从插件的默认配置生成
EnsurePluginConfigFile(plugin, configPath);
@@ -84,50 +86,125 @@ private void EnsurePluginDataDirectory(IApiPlugin plugin)
}
///
- /// 确保插件配置文件存在
- /// 如果配置文件不存在,从插件的 DefaultConfig 生成默认配置
+ /// 确保插件配置文件存在并保持与 DefaultConfig 同步
+ /// 如果配置文件不存在,从插件的 DefaultConfig 生成默认配置;
+ /// 如果已存在,则递归合并 DefaultConfig 中缺失的键,保留用户已有的值。
///
private void EnsurePluginConfigFile(IApiPlugin plugin, string configPath)
{
- // 检查配置文件是否已存在
- if (!File.Exists(configPath))
+ // 确保配置目录存在
+ var configDir = Path.GetDirectoryName(configPath);
+ if (!string.IsNullOrEmpty(configDir) && !Directory.Exists(configDir))
+ {
+ Directory.CreateDirectory(configDir);
+ }
+
+ // 获取插件提供的默认配置
+ // 注意:如果 DefaultConfig 抛出异常,属于插件自身的问题,此处不做 catch
+ var defaultConfig = plugin.DefaultConfig;
+ if (defaultConfig == null) return;
+
+ var jsonOptions = new System.Text.Json.JsonSerializerOptions
{
- // 获取插件提供的默认配置
- var defaultConfig = plugin.DefaultConfig;
+ WriteIndented = true
+ };
- if (defaultConfig != null)
+ if (!File.Exists(configPath))
+ {
+ // 配置文件不存在,直接写入 DefaultConfig
+ try
{
- try
+ var jsonString = System.Text.Json.JsonSerializer.Serialize(defaultConfig, jsonOptions);
+ if (TryWriteConfigWithTimeout(configPath, jsonString, TimeSpan.FromSeconds(10)))
{
- // 确保配置目录存在
- var configDir = Path.GetDirectoryName(configPath);
-
- if (!string.IsNullOrEmpty(configDir) && !Directory.Exists(configDir))
- {
- Directory.CreateDirectory(configDir);
- }
+ _logger.LogInformation("Generated default configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
+ }
+ else
+ {
+ _logger.LogError("Timed out while generating default configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to generate default configuration for plugin {PluginName}", plugin.Name);
+ }
+ }
+ else
+ {
+ // 配置文件已存在,读取并与 DefaultConfig 递归合并缺失的键
+ try
+ {
+ var existingJson = File.ReadAllText(configPath);
+ var existingNode = JsonNode.Parse(existingJson) as JsonObject;
+ var defaultJsonString = System.Text.Json.JsonSerializer.Serialize(defaultConfig);
+ var defaultNode = JsonNode.Parse(defaultJsonString) as JsonObject;
- // 配置 JSON 序列化选项为缩进格式,便于阅读
- var jsonOptions = new System.Text.Json.JsonSerializerOptions
- {
- WriteIndented = true
- };
+ if (existingNode != null && defaultNode != null)
+ {
+ MergeMissingKeys(existingNode, defaultNode);
+ var mergedJson = existingNode.ToJsonString(jsonOptions);
- // 将默认配置序列化为 JSON 并写入文件
- var jsonString = System.Text.Json.JsonSerializer.Serialize(defaultConfig, jsonOptions);
- if (TryWriteConfigWithTimeout(configPath, jsonString, TimeSpan.FromSeconds(10)))
+ if (TryWriteConfigWithTimeout(configPath, mergedJson, TimeSpan.FromSeconds(10)))
{
- _logger.LogInformation("Generated default configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
+ _logger.LogInformation("Merged default configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
}
else
{
- _logger.LogError("Timed out while generating default configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
+ _logger.LogError("Timed out while merging configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
}
}
- catch (Exception ex)
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to merge configuration for plugin {PluginName}, keeping existing config file unchanged", plugin.Name);
+ }
+ }
+ }
+
+ ///
+ /// 递归合并默认配置到现有配置中,仅补充缺失的键,不覆盖已有值也不删除多余键。
+ ///
+ /// 现有配置的 JsonObject
+ /// 默认配置的 JsonObject
+ private static void MergeMissingKeys(JsonObject existing, JsonObject defaults)
+ {
+ // 构建现有配置键的大小写不敏感查找表
+ // 用于检测 DefaultConfig 中的键在现有配置中是否存在但大小写不同
+ var existingKeyMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var kvp in existing)
+ {
+ existingKeyMap[kvp.Key] = kvp.Key;
+ }
+
+ foreach (var kvp in defaults)
+ {
+ if (kvp.Value == null) continue;
+
+ if (existing.ContainsKey(kvp.Key))
+ {
+ // 键存在且大小写完全匹配
+ if (kvp.Value is JsonObject defaultObj && existing[kvp.Key] is JsonObject existingObj)
+ {
+ // 双方都是嵌套对象 → 递归合并
+ MergeMissingKeys(existingObj, defaultObj);
+ }
+ // 其他情况(值类型、数组、混合类型等)→ 保留现有值,不做任何操作
+ }
+ else if (existingKeyMap.ContainsKey(kvp.Key))
+ {
+ // 存在大小写不同但实质相同的键 → 沿用现有键名
+ var actualKey = existingKeyMap[kvp.Key];
+ if (kvp.Value is JsonObject defaultObj && existing[actualKey] is JsonObject existingObj)
{
- _logger.LogError(ex, "Failed to generate default configuration for plugin {PluginName}", plugin.Name);
+ // 双方都是嵌套对象 → 递归合并到已有的键名下
+ MergeMissingKeys(existingObj, defaultObj);
}
+ // 值类型或类型不一致 → 保留现有值,不做任何操作
+ }
+ else
+ {
+ // 键完全不存在 → 添加(使用 DefaultConfig 中的键名大小写)
+ existing[kvp.Key] = kvp.Value.DeepClone();
}
}
}
@@ -140,7 +217,7 @@ private void EnsurePluginConfigFile(IApiPlugin plugin, string configPath)
public void AddSwaggerServices(string apiName, string apiVersion)
{
_services.AddEndpointsApiExplorer();
-
+
_services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo