diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 0000000..bd3fe8e
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,45 @@
+name: .NET Core Build
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["main"]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ - rid: linux-x64
+ profile: linux-x64
+ - rid: linux-arm64
+ profile: linux-arm64
+ - rid: win-x64
+ profile: win-x64
+ - rid: win-x86
+ profile: win-x86
+ - rid: win-arm64
+ profile: win-arm64
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore sharwapi.Core.csproj
+
+ - name: Publish
+ run: dotnet publish sharwapi.Core.csproj /p:PublishProfile=Properties/PublishProfiles/${{ matrix.profile }}.pubxml --configuration Release --output ./publish/${{ matrix.rid }}
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: sharwapi-core-${{ matrix.rid }}
+ path: ./publish/${{ matrix.rid }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..91a936f
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,85 @@
+name: Publish
+
+on:
+ push:
+ tags: ["v*"]
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ - rid: linux-x64
+ profile: linux-x64
+ - rid: linux-arm64
+ profile: linux-arm64
+ - rid: win-x64
+ profile: win-x64
+ - rid: win-x86
+ profile: win-x86
+ - rid: win-arm64
+ profile: win-arm64
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore sharwapi.Core.csproj
+
+ - name: Publish
+ run: dotnet publish sharwapi.Core.csproj /p:PublishProfile=Properties/PublishProfiles/${{ matrix.profile }}.pubxml --configuration Release --output ./publish/${{ matrix.rid }}
+
+ - name: Prepare package content
+ run: |
+ mkdir -p ./package/${{ matrix.rid }}
+
+ if [ -f "./publish/${{ matrix.rid }}/sharwapi.Core" ]; then
+ cp "./publish/${{ matrix.rid }}/sharwapi.Core" "./package/${{ matrix.rid }}/"
+ fi
+
+ if [ -f "./publish/${{ matrix.rid }}/sharwapi.Core.exe" ]; then
+ cp "./publish/${{ matrix.rid }}/sharwapi.Core.exe" "./package/${{ matrix.rid }}/"
+ fi
+
+ if [ ! -f "./publish/${{ matrix.rid }}/appsettings.json" ]; then
+ echo "appsettings.json not found in publish output for ${{ matrix.rid }}"
+ exit 1
+ fi
+ cp "./publish/${{ matrix.rid }}/appsettings.json" "./package/${{ matrix.rid }}/"
+
+ - name: Archive artifact
+ run: tar -czf ${{ matrix.rid }}.tar.gz -C ./package/${{ matrix.rid }} .
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ matrix.rid }}
+ path: ${{ matrix.rid }}.tar.gz
+
+ release:
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+
+ steps:
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: ./artifacts
+ merge-multiple: true
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ github.ref_name }}
+ name: ${{ github.ref_name }}
+ files: ./artifacts/*.tar.gz
+ generate_release_notes: true
diff --git a/.gitignore b/.gitignore
index 95274ae..9be1989 100644
--- a/.gitignore
+++ b/.gitignore
@@ -430,4 +430,6 @@ FodyWeavers.xsd
# Custom
-appsettings.Development.json
\ No newline at end of file
+appsettings.Development.json
+
+!Properties/PublishProfiles/*.pubxml
\ No newline at end of file
diff --git a/Modules/Configuration/AppConfiguration.cs b/Modules/Configuration/AppConfiguration.cs
new file mode 100644
index 0000000..d42daa9
--- /dev/null
+++ b/Modules/Configuration/AppConfiguration.cs
@@ -0,0 +1,52 @@
+using Microsoft.Extensions.Configuration;
+
+namespace sharwapi.Core.Modules.Configuration;
+
+///
+/// 应用程序配置构建器
+/// 负责构建和管理应用程序的配置信息
+///
+public static class AppConfiguration
+{
+ ///
+ /// 构建应用程序配置
+ ///
+ /// 配置构建器实例
+ public static IConfigurationBuilder Build()
+ {
+ return new ConfigurationBuilder()
+ .SetBasePath(AppContext.BaseDirectory)
+ .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
+ }
+
+ ///
+ /// 获取 API 名称
+ ///
+ /// 应用程序配置
+ /// API 名称
+ public static string GetApiName(IConfiguration configuration)
+ {
+ return configuration.GetValue("ApiInfo:Name") ?? "CoreAPI";
+ }
+
+ ///
+ /// 获取 API 版本
+ ///
+ /// 应用程序配置
+ /// API 版本
+ public static string GetApiVersion(IConfiguration configuration)
+ {
+ return configuration.GetValue("ApiInfo:Version") ?? "0.0.0";
+ }
+
+ ///
+ /// 获取路由前缀重写配置值
+ ///
+ /// 应用程序配置
+ /// 插件名称
+ /// 路由前缀重写值
+ public static string? GetRouteOverride(IConfiguration configuration, string pluginName)
+ {
+ return configuration.GetValue($"RouteOverride:{pluginName}");
+ }
+}
diff --git a/Modules/Hosting/ApplicationHost.cs b/Modules/Hosting/ApplicationHost.cs
new file mode 100644
index 0000000..25c2afa
--- /dev/null
+++ b/Modules/Hosting/ApplicationHost.cs
@@ -0,0 +1,127 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Serilog;
+using Serilog.Extensions.Logging;
+using sharwapi.Core.Modules.Configuration;
+using sharwapi.Core.Modules.Hosting;
+using sharwapi.Core.Modules.Logging;
+using sharwapi.Core.Modules.Middleware;
+using sharwapi.Core.Modules.PluginManagement;
+using sharwapi.Core.Modules.Routing;
+using sharwapi.Core.Modules.Services;
+using sharwapi.Contracts.Core;
+
+namespace sharwapi.Core.Modules.Hosting;
+
+///
+/// 应用程序主机
+/// 负责组装和启动整个应用程序
+///
+public class ApplicationHost
+{
+ private readonly string[] _args;
+ private IConfiguration _configuration = null!;
+ private WebApplication _app = null!;
+ private DateTime _startTime;
+ private string _apiName = null!;
+ private string _apiVersion = null!;
+
+ ///
+ /// 初始化应用程序主机
+ ///
+ /// 命令行参数
+ public ApplicationHost(string[] args)
+ {
+ _args = args;
+ _startTime = DateTime.UtcNow;
+ }
+
+ ///
+ /// 构建应用程序
+ ///
+ /// 应用程序主机实例
+ public ApplicationHost Build()
+ {
+ // 1. 构建配置
+ _configuration = AppConfiguration.Build().Build();
+
+ // 2. 初始化日志
+ Logger.Initialize(_configuration);
+ Log.Information("Starting web host");
+
+ // 3. 创建 WebApplicationBuilder
+ var builder = WebApplication.CreateBuilder(_args);
+
+ // 4. 配置主机
+ builder.Host.UseSerilogLogging();
+ builder.ConfigureHostOptions();
+
+ // 5. 获取 API 信息
+ _apiName = AppConfiguration.GetApiName(_configuration);
+ _apiVersion = AppConfiguration.GetApiVersion(_configuration);
+
+ // 6. 加载插件
+ var pluginLogger = new SerilogLoggerFactory(Log.Logger).CreateLogger("PluginLoader");
+ var pluginLoader = new PluginLoader(_configuration, pluginLogger);
+ var plugins = pluginLoader.LoadPlugins();
+
+ // 7. 检查依赖
+ var dependencyChecker = new PluginDependencyChecker(pluginLogger);
+ plugins = dependencyChecker.CheckDependencies(plugins);
+
+ // 8. 注册服务
+ // 将插件集合注入到 DI 容器(作为单例),插件实现可从容器中获取此集合
+ builder.Services.AddSingleton(plugins);
+
+ var serviceRegistrar = new PluginServiceRegistrar(builder.Services, pluginLogger);
+ serviceRegistrar.AddSwaggerServices(_apiName, _apiVersion);
+ serviceRegistrar.RegisterPluginServices(plugins, _configuration);
+
+ // 9. 构建应用
+ _app = builder.Build();
+
+ // 10. 配置中间件
+ ExceptionHandling.Configure(_app);
+ MiddlewarePipeline.Configure(_app, plugins);
+
+ // 在开发环境中启用 Swagger UI
+ if (_app.Environment.IsDevelopment())
+ {
+ _app.Logger.LogInformation("Enabling Swagger UI (Development Mode)...");
+ _app.UseSwagger();
+ _app.UseSwaggerUI(options =>
+ {
+ options.SwaggerEndpoint("/swagger/v1/swagger.json", $"{_apiName} {_apiVersion}");
+ options.RoutePrefix = "swagger";
+ });
+ }
+
+ // 11. 注册路由
+ EndpointRegistration.RegisterPluginRoutes(_app, plugins, _configuration);
+ EndpointRegistration.RegisterRootEndpoint(_app, _apiName, _apiVersion, _startTime);
+
+ return this;
+ }
+
+ ///
+ /// 运行应用程序
+ ///
+ public void Run()
+ {
+ try
+ {
+ _app.Run();
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ Log.Fatal(ex, "Host terminated unexpectedly");
+ }
+ finally
+ {
+ Log.CloseAndFlush();
+ }
+ }
+}
diff --git a/Modules/Hosting/HostingExtensions.cs b/Modules/Hosting/HostingExtensions.cs
new file mode 100644
index 0000000..9c30337
--- /dev/null
+++ b/Modules/Hosting/HostingExtensions.cs
@@ -0,0 +1,33 @@
+using Microsoft.Extensions.Hosting;
+using Serilog;
+
+namespace sharwapi.Core.Modules.Hosting;
+
+///
+/// 主机扩展方法
+///
+public static class HostingExtensions
+{
+ ///
+ /// 使用 Serilog 接管系统日志
+ ///
+ /// 主机构建器
+ /// 主机构建器
+ public static IHostBuilder UseSerilogLogging(this IHostBuilder hostBuilder)
+ {
+ return hostBuilder.UseSerilog();
+ }
+
+ ///
+ /// 配置主机选项
+ ///
+ /// Web 应用程序构建器
+ /// 关闭超时时间(秒)
+ public static void ConfigureHostOptions(this WebApplicationBuilder builder, int shutdownTimeoutSeconds = 30)
+ {
+ builder.Services.Configure(opts =>
+ {
+ opts.ShutdownTimeout = TimeSpan.FromSeconds(shutdownTimeoutSeconds);
+ });
+ }
+}
diff --git a/Modules/Logging/Logger.cs b/Modules/Logging/Logger.cs
new file mode 100644
index 0000000..dcd1514
--- /dev/null
+++ b/Modules/Logging/Logger.cs
@@ -0,0 +1,23 @@
+using Microsoft.Extensions.Configuration;
+using Serilog;
+
+namespace sharwapi.Core.Modules.Logging;
+
+///
+/// 日志服务
+/// 提供应用程序的日志记录功能,通过 appsettings.json 中的 Serilog 节点进行配置。
+///
+public static class Logger
+{
+ ///
+ /// 初始化全局 Serilog 配置。
+ /// 日志输出目标(Console、File 等)均从 appsettings.json 的 Serilog 节点读取,无需硬编码。
+ ///
+ /// 应用程序配置对象
+ public static void Initialize(IConfiguration configuration)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(configuration)
+ .CreateLogger();
+ }
+}
diff --git a/Modules/Middleware/ExceptionHandling.cs b/Modules/Middleware/ExceptionHandling.cs
new file mode 100644
index 0000000..acdc9f8
--- /dev/null
+++ b/Modules/Middleware/ExceptionHandling.cs
@@ -0,0 +1,46 @@
+using Microsoft.AspNetCore.Diagnostics;
+
+namespace sharwapi.Core.Modules.Middleware;
+
+///
+/// 全局异常处理中间件配置
+///
+public static class ExceptionHandling
+{
+ ///
+ /// 配置全局异常处理
+ ///
+ /// Web 应用程序
+ public static void Configure(WebApplication app)
+ {
+ app.UseExceptionHandler(exceptionHandlerApp =>
+ {
+ exceptionHandlerApp.Run(async context =>
+ {
+ context.Response.StatusCode = StatusCodes.Status500InternalServerError;
+ context.Response.ContentType = "application/json";
+ var exceptionDetails = context.Features.Get();
+ var exception = exceptionDetails?.Error;
+
+ if (exception != null)
+ {
+ app.Logger.LogError(exception, "Unhandled exception caught by global handler at {Path}", exceptionDetails?.Path);
+ }
+ else
+ {
+ app.Logger.LogError("Unhandled exception caught by global handler at {Path}, but exception details were unavailable.", exceptionDetails?.Path);
+ }
+
+ var response = new
+ {
+ StatusCode = context.Response.StatusCode,
+ Message = "An unexpected internal server error has occurred.",
+ Details = app.Environment.IsDevelopment() ? exception?.Message : null,
+ Path = exceptionDetails?.Path
+ };
+
+ await context.Response.WriteAsJsonAsync(response);
+ });
+ });
+ }
+}
diff --git a/Modules/Middleware/MiddlewarePipeline.cs b/Modules/Middleware/MiddlewarePipeline.cs
new file mode 100644
index 0000000..d906aa4
--- /dev/null
+++ b/Modules/Middleware/MiddlewarePipeline.cs
@@ -0,0 +1,32 @@
+using Microsoft.AspNetCore.Builder;
+using sharwapi.Contracts.Core;
+
+namespace sharwapi.Core.Modules.Middleware;
+
+///
+/// 插件中间件管道配置
+///
+public static class MiddlewarePipeline
+{
+ ///
+ /// 配置所有插件的中间件
+ ///
+ /// Web 应用程序
+ /// 已加载的插件列表
+ public static void Configure(WebApplication app, List plugins)
+ {
+ app.Logger.LogInformation("Configuring plugin middleware...");
+
+ foreach (var plugin in plugins)
+ {
+ try
+ {
+ plugin.Configure(app);
+ }
+ catch (Exception ex)
+ {
+ app.Logger.LogError(ex, "Plugin '{PluginName}' threw an exception during Configure().", plugin.Name);
+ }
+ }
+ }
+}
diff --git a/Modules/PluginManagement/PluginDependencyChecker.cs b/Modules/PluginManagement/PluginDependencyChecker.cs
new file mode 100644
index 0000000..f2a50f3
--- /dev/null
+++ b/Modules/PluginManagement/PluginDependencyChecker.cs
@@ -0,0 +1,295 @@
+using System.Collections.ObjectModel;
+using Microsoft.Extensions.Logging;
+using NuGet.Versioning;
+using sharwapi.Contracts.Core;
+
+namespace sharwapi.Core.Modules.PluginManagement;
+
+///
+/// 插件依赖检查器
+/// 负责检查插件的依赖关系是否满足,并进行拓扑排序以确定加载顺序
+///
+public class PluginDependencyChecker
+{
+ private readonly ILogger _logger;
+
+ ///
+ /// 初始化依赖检查器
+ ///
+ /// 日志记录器
+ public PluginDependencyChecker(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ /// 检查所有插件的依赖关系
+ ///
+ /// 所有候选插件列表
+ /// 通过依赖检查的有效插件列表(已拓扑排序)
+ public List CheckDependencies(List plugins)
+ {
+ if (plugins.Count == 0)
+ return plugins;
+
+ // 步骤1:构建依赖图
+ var dependencyGraph = BuildDependencyGraph(plugins);
+
+ // 步骤2:检测循环依赖(使用 Kahn 算法)
+ var sortedPlugins = TopologicalSort(dependencyGraph, plugins);
+
+ if (sortedPlugins == null)
+ {
+ // 循环依赖已被 Kahn 算法检测并记录,返回空列表
+ return new List();
+ }
+
+ // 步骤3:阶段一 - 声明式强依赖检查(基于拓扑排序结果)
+ var stageOneValid = new Dictionary();
+ foreach (var plugin in sortedPlugins)
+ {
+ if (CheckDeclarativeDependencies(plugin, dependencyGraph, stageOneValid))
+ {
+ stageOneValid[plugin.Name] = plugin;
+ }
+ else
+ {
+ _logger.LogWarning("Plugin '{PluginName}' failed stage one (declarative) dependency check and was removed.", plugin.Name);
+ }
+ }
+
+ // 步骤4:阶段二 - 自定义验证(只对阶段一通过的插件调用)
+ // 传入的是已通过阶段一的"有效候选",而非全部候选
+ var stageTwoValid = new Dictionary();
+ foreach (var plugin in stageOneValid.Values)
+ {
+ try
+ {
+ // 传入阶段一的有效插件列表
+ if (!plugin.ValidateDependency(new ReadOnlyDictionary(
+ stageOneValid.ToDictionary(p => p.Key, p => p.Value.Version))))
+ {
+ _logger.LogWarning("Plugin '{PluginName}' rejected loading during stage two (ValidateDependency) check.", plugin.Name);
+ }
+ else
+ {
+ stageTwoValid[plugin.Name] = plugin;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Plugin '{PluginName}' threw an exception during stage two dependency validation.", plugin.Name);
+ }
+ }
+
+ // 步骤5:级联剔除 - 如果某个插件被剔除,依赖它的插件也需要重新检查
+ var finalValid = ApplyCascadeRemoval(stageTwoValid, dependencyGraph);
+
+ // 记录结果
+ if (finalValid.Count < plugins.Count)
+ {
+ var removedCount = plugins.Count - finalValid.Count;
+ _logger.LogWarning("{Count} plugins were unloaded due to missing or incompatible dependencies.", removedCount);
+ }
+
+ // 返回拓扑排序后的有效插件列表
+ return sortedPlugins.Where(p => finalValid.ContainsKey(p.Name)).ToList();
+ }
+
+ ///
+ /// 构建依赖图
+ ///
+ private Dictionary> BuildDependencyGraph(List plugins)
+ {
+ var graph = new Dictionary>();
+
+ foreach (var plugin in plugins)
+ {
+ if (!graph.ContainsKey(plugin.Name))
+ {
+ graph[plugin.Name] = new HashSet();
+ }
+
+ foreach (var dep in plugin.Dependencies)
+ {
+ graph[plugin.Name].Add(dep.Key);
+ }
+ }
+
+ return graph;
+ }
+
+ ///
+ /// 使用 Kahn 算法进行拓扑排序,同时检测循环依赖
+ ///
+ private List? TopologicalSort(Dictionary> graph, List plugins)
+ {
+ var pluginMap = plugins.ToDictionary(p => p.Name);
+
+ // 计算每个插件的入度(被依赖数)
+ var inDegree = new Dictionary();
+ foreach (var plugin in plugins)
+ {
+ inDegree[plugin.Name] = 0;
+ }
+
+ foreach (var plugin in plugins)
+ {
+ foreach (var dep in graph[plugin.Name])
+ {
+ // 如果被依赖的插件存在于候选列表中,增加其入度
+ if (inDegree.ContainsKey(dep))
+ {
+ inDegree[dep]++;
+ }
+ }
+ }
+
+ // 入度为0的插件可以首先加载(没有插件依赖它)
+ var queue = new Queue(inDegree.Where(kv => kv.Value == 0).Select(kv => kv.Key));
+ var sorted = new List();
+
+ while (queue.Count > 0)
+ {
+ var current = queue.Dequeue();
+ sorted.Add(current);
+
+ // 找到所有依赖当前插件的插件
+ foreach (var plugin in plugins)
+ {
+ if (graph[plugin.Name].Contains(current))
+ {
+ inDegree[plugin.Name]--;
+ if (inDegree[plugin.Name] == 0)
+ {
+ queue.Enqueue(plugin.Name);
+ }
+ }
+ }
+ }
+
+ // 如果排序后的数量不等于插件数量,说明存在循环依赖
+ if (sorted.Count != plugins.Count)
+ {
+ // 找出循环依赖的插件
+ var cyclicPlugins = inDegree.Where(kv => kv.Value > 0).Select(kv => kv.Key);
+ _logger.LogError("Circular dependency detected among plugins: {CyclicPlugins}. Loading aborted.",
+ string.Join(", ", cyclicPlugins));
+ return null;
+ }
+
+ // 按排序结果返回插件列表
+ return sorted.Select(name => pluginMap[name]).ToList();
+ }
+
+ ///
+ /// 阶段一:声明式强依赖检查
+ /// 只检查插件声明的 Dependencies 是否满足
+ ///
+ private bool CheckDeclarativeDependencies(IApiPlugin plugin,
+ Dictionary> graph,
+ Dictionary validPlugins)
+ {
+ foreach (var dependency in plugin.Dependencies)
+ {
+ if (!CheckSingleDependency(plugin, dependency, validPlugins))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// 检查单个依赖(使用当前有效插件列表)
+ ///
+ private bool CheckSingleDependency(IApiPlugin plugin, KeyValuePair dependency, Dictionary validPlugins)
+ {
+ string depName = dependency.Key;
+ string depRangeStr = dependency.Value ?? string.Empty;
+
+ // 检查依赖插件是否在有效列表中
+ if (!validPlugins.TryGetValue(depName, out var depPlugin))
+ {
+ _logger.LogError("Plugin '{PluginName}' failed to load. Missing dependency: '{DepName}'.", plugin.Name, depName);
+ return false;
+ }
+
+ // 解析版本
+ if (!NuGetVersion.TryParse(depPlugin.Version, out var loadedVersion))
+ {
+ _logger.LogError("Plugin '{PluginName}' depends on '{DepName}', but the loaded version '{LoadedVer}' has an invalid format.",
+ plugin.Name, depName, depPlugin.Version);
+ return false;
+ }
+
+ // 解析版本范围
+ if (!VersionRange.TryParse(depRangeStr, out var requiredRange))
+ {
+ if (FloatRange.TryParse(depRangeStr, out var floatRange))
+ {
+ if (!floatRange.Satisfies(loadedVersion))
+ {
+ _logger.LogError("Plugin '{PluginName}' requires '{DepName}' version '{DepRange}' (Floating), but loaded version '{LoadedVer}' is incompatible.",
+ plugin.Name, depName, depRangeStr, loadedVersion);
+ return false;
+ }
+ }
+ else
+ {
+ _logger.LogError("Plugin '{PluginName}' has an invalid dependency version format for '{DepName}': '{DepRange}'.",
+ plugin.Name, depName, depRangeStr);
+ return false;
+ }
+ }
+ else if (!requiredRange!.Satisfies(loadedVersion))
+ {
+ _logger.LogError("Plugin '{PluginName}' requires '{DepName}' version '{DepRange}', but loaded version '{LoadedVer}' is incompatible.",
+ plugin.Name, depName, depRangeStr, loadedVersion);
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// 级联剔除:当某个插件被剔除时,依赖它的插件也需要重新检查
+ ///
+ private Dictionary ApplyCascadeRemoval(Dictionary validPlugins,
+ Dictionary> graph)
+ {
+ var result = new Dictionary(validPlugins);
+ bool changed;
+
+ do
+ {
+ changed = false;
+ var toRemove = new List();
+
+ foreach (var plugin in result.Values)
+ {
+ // 检查这个插件依赖的所有插件是否都还在结果中
+ foreach (var depName in graph[plugin.Name])
+ {
+ if (!result.ContainsKey(depName))
+ {
+ // 依赖缺失,需要剔除此插件
+ toRemove.Add(plugin.Name);
+ _logger.LogWarning("Plugin '{PluginName}' is being removed due to missing dependency '{DepName}'.",
+ plugin.Name, depName);
+ break;
+ }
+ }
+ }
+
+ foreach (var name in toRemove)
+ {
+ result.Remove(name);
+ changed = true;
+ }
+
+ } while (changed);
+
+ return result;
+ }
+}
diff --git a/Modules/PluginManagement/PluginLoadContext.cs b/Modules/PluginManagement/PluginLoadContext.cs
new file mode 100644
index 0000000..d9966ef
--- /dev/null
+++ b/Modules/PluginManagement/PluginLoadContext.cs
@@ -0,0 +1,68 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace sharwapi.Core.Modules.PluginManagement;
+
+///
+/// 自定义程序集加载上下文,用于实现插件的隔离加载。
+/// 每个插件在独立的 AssemblyLoadContext 实例中运行,防止依赖冲突。
+///
+public class PluginLoadContext : AssemblyLoadContext
+{
+ ///
+ /// 依赖解析器,用于根据 .deps.json 文件解析依赖程序集的路径。
+ ///
+ private readonly AssemblyDependencyResolver _resolver;
+
+ ///
+ /// 初始化 PluginLoadContext 的新实例。
+ ///
+ /// 插件主程序集文件的完整路径。
+ public PluginLoadContext(string pluginPath) : base(isCollectible: true)
+ {
+ _resolver = new AssemblyDependencyResolver(pluginPath);
+ }
+
+ ///
+ /// 重写 Load 方法以自定义程序集加载逻辑。
+ ///
+ /// 程序集名称。
+ /// 加载的程序集实例,如果无法解析则返回 null。
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ // 确保 sharwapi.Contracts.Core 程序集不被当前上下文加载。
+ // 这保证了宿主应用程序和插件使用相同的 IApiPlugin 接口类型,
+ // 避免因类型加载上下文不同而导致的类型转换异常。
+ if (assemblyName.Name == "sharwapi.Contracts.Core")
+ {
+ return null;
+ }
+
+ // 尝试使用依赖解析器将程序集名称解析为文件路径。
+ string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
+
+ if (assemblyPath != null)
+ {
+ return LoadFromAssemblyPath(assemblyPath);
+ }
+
+ return null;
+ }
+
+ ///
+ /// 重写 LoadUnmanagedDll 方法以自定义非托管库加载逻辑。
+ ///
+ /// 非托管库名称。
+ /// 加载的库指针,如果无法解析则返回 IntPtr.Zero。
+ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
+ {
+ string? libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
+
+ if (libraryPath != null)
+ {
+ return LoadUnmanagedDllFromPath(libraryPath);
+ }
+
+ return IntPtr.Zero;
+ }
+}
diff --git a/Modules/PluginManagement/PluginLoader.cs b/Modules/PluginManagement/PluginLoader.cs
new file mode 100644
index 0000000..a0f37de
--- /dev/null
+++ b/Modules/PluginManagement/PluginLoader.cs
@@ -0,0 +1,100 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using sharwapi.Contracts.Core;
+
+namespace sharwapi.Core.Modules.PluginManagement;
+
+///
+/// 插件加载器
+/// 负责从 Plugins 目录加载实现了 IApiPlugin 的程序集
+///
+public class PluginLoader
+{
+ private readonly IConfiguration _configuration;
+ private readonly ILogger _logger;
+
+ ///
+ /// 初始化插件加载器
+ ///
+ /// 应用程序配置
+ /// 日志记录器
+ public PluginLoader(IConfiguration configuration, ILogger logger)
+ {
+ _configuration = configuration;
+ _logger = logger;
+ }
+
+ ///
+ /// 加载所有插件
+ ///
+ /// 加载的插件列表
+ public List LoadPlugins()
+ {
+ var loadedPlugins = new List();
+
+ // 获取插件目录路径
+ string pluginsPath = Path.Combine(AppContext.BaseDirectory, "Plugins");
+
+ // 检查插件目录是否存在,如果不存在则创建
+ if (!Directory.Exists(pluginsPath))
+ {
+ try
+ {
+ Directory.CreateDirectory(pluginsPath);
+ _logger.LogInformation("Plugins directory did not exist and was created at {PluginsPath}", pluginsPath);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to create plugins directory at {PluginsPath}", pluginsPath);
+ }
+ return loadedPlugins;
+ }
+
+ // 遍历 Plugins 目录下的所有 DLL 文件
+ foreach (var dllPath in Directory.GetFiles(pluginsPath, "*.dll"))
+ {
+ try
+ {
+ // 尝试从每个 DLL 加载插件
+ var plugin = LoadPluginFromPath(dllPath);
+ if (plugin != null)
+ {
+ loadedPlugins.Add(plugin);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error loading plugin from {DllPath}", dllPath);
+ }
+ }
+
+ return loadedPlugins;
+ }
+
+ ///
+ /// 从指定路径加载单个插件
+ ///
+ /// DLL 文件路径
+ /// 插件实例,如果加载失败则返回 null
+ private IApiPlugin? LoadPluginFromPath(string dllPath)
+ {
+ // 使用隔离加载上下文加载程序集,防止插件之间的依赖冲突
+ var loadContext = new PluginLoadContext(dllPath);
+ var assembly = loadContext.LoadFromAssemblyPath(dllPath);
+
+ // 从程序集中查找所有实现了 IApiPlugin 接口的类型
+ var pluginTypes = assembly.GetTypes()
+ .Where(t => typeof(IApiPlugin).IsAssignableFrom(t) && !t.IsInterface);
+
+ // 遍历找到的类型,尝试实例化第一个成功的类型
+ foreach (var type in pluginTypes)
+ {
+ if (Activator.CreateInstance(type) is IApiPlugin plugin)
+ {
+ return plugin;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/Modules/Routing/EndpointRegistration.cs b/Modules/Routing/EndpointRegistration.cs
new file mode 100644
index 0000000..9f25f94
--- /dev/null
+++ b/Modules/Routing/EndpointRegistration.cs
@@ -0,0 +1,64 @@
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Configuration;
+using sharwapi.Contracts.Core;
+
+namespace sharwapi.Core.Modules.Routing;
+
+///
+/// 端点注册
+/// 负责注册插件路由和根路径端点
+///
+public static class EndpointRegistration
+{
+ ///
+ /// 注册所有插件的路由
+ ///
+ /// Web 应用程序
+ /// 已加载的插件列表
+ /// 应用程序配置(用于解析路由前缀覆盖)
+ public static void RegisterPluginRoutes(WebApplication app, List plugins, IConfiguration configuration)
+ {
+ app.Logger.LogInformation("Registering plugin routes...");
+
+ // 遍历所有已加载的插件,为每个插件注册路由
+ foreach (var plugin in plugins)
+ {
+ try
+ {
+ // 解析插件的路由前缀(可能来自配置覆盖,使用全局配置)
+ var routeBuilder = RoutePrefixResolver.Resolve(plugin, configuration, app);
+
+ // 为插件构建专属配置(来自 config/{插件名}.json)
+ var configPath = Path.Combine(AppContext.BaseDirectory, "config", $"{plugin.Name}.json");
+ var pluginConfig = new ConfigurationBuilder()
+ .AddJsonFile(configPath, optional: true, reloadOnChange: true)
+ .Build();
+
+ // 调用插件的路由注册方法,传入插件专属配置
+ plugin.RegisterRoutes(routeBuilder, pluginConfig);
+
+ app.Logger.LogInformation("Loaded Plugin: {PluginName} v{PluginVersion}", plugin.Name, plugin.Version);
+ }
+ catch (Exception ex)
+ {
+ app.Logger.LogError(ex, "Plugin '{PluginName}' threw an exception during RegisterRoutes().", plugin.Name);
+ }
+ }
+ }
+
+ ///
+ /// 注册根路径端点
+ ///
+ /// Web 应用程序
+ /// API 名称
+ /// API 版本
+ /// 应用启动时间
+ public static void RegisterRootEndpoint(WebApplication app, string apiName, string apiVersion, DateTime startTime)
+ {
+ app.MapGet("/", () =>
+ {
+ var uptime = DateTime.UtcNow - startTime;
+ return new { apiName, version = apiVersion, runningTime = uptime, message = "Core API running." };
+ });
+ }
+}
diff --git a/Modules/Routing/RoutePrefixResolver.cs b/Modules/Routing/RoutePrefixResolver.cs
new file mode 100644
index 0000000..0895141
--- /dev/null
+++ b/Modules/Routing/RoutePrefixResolver.cs
@@ -0,0 +1,52 @@
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Configuration;
+using sharwapi.Contracts.Core;
+
+namespace sharwapi.Core.Modules.Routing;
+
+///
+/// 路由前缀解析器
+/// 负责解析插件的路由前缀
+///
+public static class RoutePrefixResolver
+{
+ ///
+ /// 解析插件的路由前缀
+ /// 根据插件配置和应用程序配置确定路由前缀
+ ///
+ /// 插件实例
+ /// 应用程序配置
+ /// Web 应用程序
+ /// 路由构建器(可能是分组路由)
+ public static IEndpointRouteBuilder Resolve(IApiPlugin plugin, IConfiguration configuration, WebApplication app)
+ {
+ // 如果插件不启用自动路由前缀,直接返回 app
+ if (!plugin.UseAutoRoutePrefix)
+ {
+ return app;
+ }
+
+ // 默认使用插件名称作为路由前缀
+ string routePrefix = plugin.Name;
+
+ // 检查是否存在路由前缀覆盖配置
+ var overrideRoute = configuration.GetValue($"RouteOverride:{plugin.Name}");
+ if (!string.IsNullOrEmpty(overrideRoute))
+ {
+ // 验证覆盖值只包含字母和数字
+ if (System.Text.RegularExpressions.Regex.IsMatch(overrideRoute, "^[a-zA-Z0-9]+$"))
+ {
+ routePrefix = overrideRoute;
+ app.Logger.LogInformation("Route prefix for plugin '{PluginName}' overridden to '{RoutePrefix}'", plugin.Name, routePrefix);
+ }
+ else
+ {
+ // 无效的覆盖值,回退到插件名称
+ app.Logger.LogWarning("Invalid route override '{OverrideRoute}' for plugin '{PluginName}'. Only alphanumeric characters (A-Z, a-z, 0-9) are allowed. Falling back to default.", overrideRoute, plugin.Name);
+ }
+ }
+
+ // 返回带有前缀的路由组
+ return app.MapGroup($"/{routePrefix.TrimStart('/')}");
+ }
+}
diff --git a/Modules/Services/ServiceRegistration.cs b/Modules/Services/ServiceRegistration.cs
new file mode 100644
index 0000000..dbfc0a9
--- /dev/null
+++ b/Modules/Services/ServiceRegistration.cs
@@ -0,0 +1,186 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using sharwapi.Contracts.Core;
+
+namespace sharwapi.Core.Modules.Services;
+
+///
+/// 插件服务注册器
+/// 负责将插件的服务注册到 DI 容器
+///
+public class PluginServiceRegistrar
+{
+ private readonly IServiceCollection _services;
+ private readonly Microsoft.Extensions.Logging.ILogger _logger;
+
+ ///
+ /// 初始化服务注册器
+ ///
+ /// 服务集合
+ /// 日志记录器
+ public PluginServiceRegistrar(IServiceCollection services, Microsoft.Extensions.Logging.ILogger logger)
+ {
+ _services = services;
+ _logger = logger;
+ }
+
+ ///
+ /// 注册所有插件的服务
+ ///
+ /// 已加载的插件列表
+ /// 应用程序配置
+ public void RegisterPluginServices(List plugins, IConfiguration configuration)
+ {
+ _logger.LogInformation("Registering plugin services...");
+
+ foreach (var plugin in plugins)
+ {
+ RegisterPluginServices(plugin, configuration);
+ }
+ }
+
+ ///
+ /// 注册单个插件的服务
+ ///
+ private void RegisterPluginServices(IApiPlugin plugin, IConfiguration configuration)
+ {
+ // 构建插件配置文件的完整路径
+ var configPath = Path.Combine(AppContext.BaseDirectory, "config", $"{plugin.Name}.json");
+
+ // 确保配置文件存在,如果不存在则从插件的默认配置生成
+ EnsurePluginConfigFile(plugin, configPath);
+
+ // 为插件构建独立的配置对象,支持热重载
+ var pluginConfig = new ConfigurationBuilder()
+ .AddJsonFile(configPath, optional: true, reloadOnChange: true)
+ .Build();
+
+ // 调用插件的服务注册方法
+ plugin.RegisterServices(_services, pluginConfig);
+ }
+
+ ///
+ /// 确保插件配置文件存在
+ /// 如果配置文件不存在,从插件的 DefaultConfig 生成默认配置
+ ///
+ private void EnsurePluginConfigFile(IApiPlugin plugin, string configPath)
+ {
+ // 检查配置文件是否已存在
+ if (!File.Exists(configPath))
+ {
+ // 获取插件提供的默认配置
+ var defaultConfig = plugin.DefaultConfig;
+
+ if (defaultConfig != null)
+ {
+ try
+ {
+ // 确保配置目录存在
+ var configDir = Path.GetDirectoryName(configPath);
+
+ if (!string.IsNullOrEmpty(configDir) && !Directory.Exists(configDir))
+ {
+ Directory.CreateDirectory(configDir);
+ }
+
+ // 配置 JSON 序列化选项为缩进格式,便于阅读
+ var jsonOptions = new System.Text.Json.JsonSerializerOptions
+ {
+ WriteIndented = true
+ };
+
+ // 将默认配置序列化为 JSON 并写入文件
+ var jsonString = System.Text.Json.JsonSerializer.Serialize(defaultConfig, jsonOptions);
+ if (TryWriteConfigWithTimeout(configPath, jsonString, TimeSpan.FromSeconds(10)))
+ {
+ _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);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 添加 Swagger/OpenAPI 服务
+ ///
+ /// API 名称
+ /// API 版本
+ public void AddSwaggerServices(string apiName, string apiVersion)
+ {
+ _services.AddEndpointsApiExplorer();
+
+ _services.AddSwaggerGen(options =>
+ {
+ options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
+ {
+ Title = apiName,
+ Version = apiVersion,
+ Description = "一个由插件动态构建的 API"
+ });
+
+ options.AddSecurityDefinition("ApiKeyAuth", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
+ {
+ Name = "X-Api-Token",
+ In = Microsoft.OpenApi.Models.ParameterLocation.Header,
+ Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey,
+ Description = "用于访问受保护路由的 API 令牌"
+ });
+
+ options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
+ {
+ {
+ new Microsoft.OpenApi.Models.OpenApiSecurityScheme
+ {
+ Reference = new Microsoft.OpenApi.Models.OpenApiReference
+ {
+ Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
+ Id = "ApiKeyAuth"
+ }
+ },
+ new List()
+ }
+ });
+ });
+ }
+ ///
+ /// 以超时保护写入配置文件,避免启动阶段因 I/O 卡顿而无限阻塞。
+ ///
+ private static bool TryWriteConfigWithTimeout(string configPath, string jsonContent, TimeSpan timeout)
+ {
+ try
+ {
+ using var cts = new CancellationTokenSource(timeout);
+ var writeTask = WriteConfigAsync(configPath, jsonContent, cts.Token);
+ writeTask.GetAwaiter().GetResult();
+ return true;
+ }
+ catch (OperationCanceledException)
+ {
+ return false;
+ }
+ }
+
+ private static async Task WriteConfigAsync(string configPath, string jsonContent, CancellationToken cancellationToken)
+ {
+ await using var fileStream = new FileStream(
+ configPath,
+ FileMode.Create,
+ FileAccess.Write,
+ FileShare.None,
+ bufferSize: 4096,
+ options: FileOptions.Asynchronous | FileOptions.WriteThrough);
+
+ var bytes = System.Text.Encoding.UTF8.GetBytes(jsonContent);
+ await fileStream.WriteAsync(bytes, cancellationToken);
+ await fileStream.FlushAsync(cancellationToken);
+ }
+}
diff --git a/Program.cs b/Program.cs
index e0bd1b7..d80c985 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,190 +1,4 @@
-using Microsoft.AspNetCore.Diagnostics;
-using sharwapi.Contracts.Core;
-using System.Reflection;
-using Microsoft.OpenApi.Models;
+using sharwapi.Core.Modules.Hosting;
-// ڼ¼ʱʱuptime
-var startTime = DateTime.UtcNow;
-
-// WebApplicationBuilderӦڣ
-var builder = WebApplication.CreateBuilder(args);
-
-builder.Services.Configure(opts =>
-{
- opts.ShutdownTimeout = TimeSpan.FromSeconds(30);
-});
-
-// δһʱ LoggerFactoryڼ¼ؽε־
-using var bootstrapLoggerFactory = LoggerFactory.Create(b =>
-{
- b.AddConsole();
-});
-var pluginLoaderLogger = bootstrapLoggerFactory.CreateLogger("PluginLoader");
-
-// жȡ API Ϣ汾δʹĬֵ
-var apiName = builder.Configuration.GetValue("ApiInfo:Name") ?? "CoreAPI";
-var apiVersion = builder.Configuration.GetValue("ApiInfo:Version") ?? "0.0.0";
-
-// λĿ¼ Plugins Ŀ¼IJDLL
-var plugins = LoadPlugins(builder.Configuration, pluginLoaderLogger);
-
-// ע뵽 DI Ϊʵֿɴлȡ˼
-builder.Services.AddSingleton(plugins);
-
-// ÿ DI עԼķ
-pluginLoaderLogger.LogInformation("Registering plugin services...");
-foreach (var plugin in plugins)
-{
- plugin.RegisterServices(builder.Services, builder.Configuration);
-}
-
-// ӻ OpenAPI/Swagger ֧
-builder.Services.AddEndpointsApiExplorer();
-
-builder.Services.AddSwaggerGen(options =>
-{
- // Swagger ĵϢ
- options.SwaggerDoc("v1", new OpenApiInfo
- {
- Title = apiName,
- Version = apiVersion,
- Description = "һɲ̬ API"
- });
-
- // Զ ApiKey ȫ壨ͷ X-Api-Tokenʹܱ·
- options.AddSecurityDefinition("ApiKeyAuth", new OpenApiSecurityScheme
- {
- Name = "X-Api-Token",
- In = ParameterLocation.Header,
- Type = SecuritySchemeType.ApiKey,
- Description = "ڷܱ·ɵ API "
- });
-
- // Ӧõȫ֣˴ָضΧ
- options.AddSecurityRequirement(new OpenApiSecurityRequirement
- {
- {
- new OpenApiSecurityScheme
- {
- Reference = new OpenApiReference
- {
- Type = ReferenceType.SecurityScheme,
- Id = "ApiKeyAuth"
- }
- },
- new List()
- }
- });
-});
-
-// WebApplication ʵʱעᣩ
-var app = builder.Build();
-
-// ȫ쳣δ쳣ͳһ JSON Ӧ
-app.UseExceptionHandler(exceptionHandlerApp =>
-{
- exceptionHandlerApp.Run(async context =>
- {
- context.Response.StatusCode = StatusCodes.Status500InternalServerError;
- context.Response.ContentType = "application/json";
- var exceptionDetails = context.Features.Get();
- var exception = exceptionDetails?.Error;
- // ¼쳣־а·
- app.Logger.LogError(exception, "Unhandled exception caught by global handler at {Path}", exceptionDetails?.Path);
- var response = new
- {
- StatusCode = context.Response.StatusCode,
- Message = "An unexpected internal server error has occurred.",
-
- // ڿзϸϢй¶ڲ쳣Ϣ
- Details = app.Environment.IsDevelopment() ? exception?.Message : null,
- Path = exceptionDetails?.Path
- };
- await context.Response.WriteAsJsonAsync(response);
- });
-});
-
-// ڿ Swagger UIڵ鿴ĵ
-if (app.Environment.IsDevelopment())
-{
- app.Logger.LogInformation("Enabling Swagger UI (Development Mode)...");
- app.UseSwagger();
- app.UseSwaggerUI(options =>
- {
- options.SwaggerEndpoint("/swagger/v1/swagger.json", $"{apiName} {apiVersion}");
- options.RoutePrefix = "swagger"; // /swagger Ϊ UI ·ǰ
- });
-}
-
-// ÿлܵעм
-app.Logger.LogInformation("Configuring plugin middleware...");
-foreach (var plugin in plugins)
-{
- plugin.Configure(app);
-}
-
-// ÿע·˵
-app.Logger.LogInformation("Registering plugin routes...");
-foreach (var plugin in plugins)
-{
- plugin.RegisterRoutes(app, app.Configuration);
- app.Logger.LogInformation("Loaded Plugin: {PluginName} v{PluginVersion}", plugin.Name, plugin.Version);
-}
-
-// ·ʾ API ơ汾ʱ
-app.MapGet("/", () =>
-{
- var uptime = DateTime.UtcNow - startTime;
- return new { apiName, version = apiVersion, runningTime = uptime, message = "Core API running." };
-});
-
-//
-app.Run();
-
-// Plugins Ŀ¼ʵ IApiPlugin Ͳʵ
-List LoadPlugins(IConfiguration configuration, ILogger logger)
-{
- var loadedPlugins = new List();
- string pluginsPath = Path.Combine(AppContext.BaseDirectory, "Plugins");
-
- if (!Directory.Exists(pluginsPath))
- {
- try
- {
- Directory.CreateDirectory(pluginsPath);
- logger.LogInformation("Plugins directory did not exist and was created at {PluginsPath}", pluginsPath);
- }
- catch (Exception ex)
- {
- // Ŀ¼ʱ¼ؿղб
- logger.LogError(ex, "Failed to create plugins directory at {PluginsPath}", pluginsPath);
- return loadedPlugins;
- }
- return loadedPlugins;
- }
-
- foreach (var dllPath in Directory.GetFiles(pluginsPath, "*.dll"))
- {
- try
- {
- // Լʽزʵ IApiPlugin
- var assembly = Assembly.LoadFrom(dllPath);
- var pluginTypes = assembly.GetTypes()
- .Where(t => typeof(IApiPlugin).IsAssignableFrom(t) && !t.IsInterface);
-
- foreach (var type in pluginTypes)
- {
- if (Activator.CreateInstance(type) is IApiPlugin plugin)
- {
- loadedPlugins.Add(plugin);
- }
- }
- }
- catch (Exception ex)
- {
- // ʧʱ¼
- logger.LogError(ex, "Error loading plugin from {DllPath}", dllPath);
- }
- }
- return loadedPlugins;
-}
\ No newline at end of file
+var applicationHost = new ApplicationHost(args);
+applicationHost.Build().Run();
diff --git a/Properties/PublishProfiles/Portable.pubxml b/Properties/PublishProfiles/Portable.pubxml
new file mode 100644
index 0000000..dad14ed
--- /dev/null
+++ b/Properties/PublishProfiles/Portable.pubxml
@@ -0,0 +1,13 @@
+
+
+
+
+ Release
+ AnyCPU
+ FileSystem
+ net10.0
+ $(PublishRid)
+ false
+ false
+
+
diff --git a/Properties/PublishProfiles/linux-arm64.pubxml b/Properties/PublishProfiles/linux-arm64.pubxml
new file mode 100644
index 0000000..93f980d
--- /dev/null
+++ b/Properties/PublishProfiles/linux-arm64.pubxml
@@ -0,0 +1,14 @@
+
+
+
+
+ Release
+ AnyCPU
+ FileSystem
+ net10.0
+ linux-arm64
+ true
+ true
+ false
+
+
diff --git a/Properties/PublishProfiles/linux-x64.pubxml b/Properties/PublishProfiles/linux-x64.pubxml
new file mode 100644
index 0000000..4749c34
--- /dev/null
+++ b/Properties/PublishProfiles/linux-x64.pubxml
@@ -0,0 +1,14 @@
+
+
+
+
+ Release
+ AnyCPU
+ FileSystem
+ net10.0
+ linux-x64
+ true
+ true
+ false
+
+
diff --git a/Properties/PublishProfiles/macos-arm64.pubxml b/Properties/PublishProfiles/macos-arm64.pubxml
new file mode 100644
index 0000000..cd70a77
--- /dev/null
+++ b/Properties/PublishProfiles/macos-arm64.pubxml
@@ -0,0 +1,13 @@
+
+
+
+
+ Release
+ AnyCPU
+ FileSystem
+ net10.0
+ osx-arm64
+ true
+ false
+
+
diff --git a/Properties/PublishProfiles/macos-x64.pubxml b/Properties/PublishProfiles/macos-x64.pubxml
new file mode 100644
index 0000000..1cd935b
--- /dev/null
+++ b/Properties/PublishProfiles/macos-x64.pubxml
@@ -0,0 +1,13 @@
+
+
+
+
+ Release
+ AnyCPU
+ FileSystem
+ net10.0
+ osx-x64
+ true
+ false
+
+
diff --git a/Properties/PublishProfiles/win-arm64.pubxml b/Properties/PublishProfiles/win-arm64.pubxml
new file mode 100644
index 0000000..ca61ab4
--- /dev/null
+++ b/Properties/PublishProfiles/win-arm64.pubxml
@@ -0,0 +1,14 @@
+
+
+
+
+ Release
+ AnyCPU
+ FileSystem
+ net10.0
+ win-arm64
+ true
+ true
+ false
+
+
diff --git a/Properties/PublishProfiles/win-x64.pubxml b/Properties/PublishProfiles/win-x64.pubxml
new file mode 100644
index 0000000..d50680c
--- /dev/null
+++ b/Properties/PublishProfiles/win-x64.pubxml
@@ -0,0 +1,14 @@
+
+
+
+
+ Release
+ AnyCPU
+ FileSystem
+ net10.0
+ win-x64
+ true
+ true
+ false
+
+
diff --git a/Properties/PublishProfiles/win-x86.pubxml b/Properties/PublishProfiles/win-x86.pubxml
new file mode 100644
index 0000000..996f599
--- /dev/null
+++ b/Properties/PublishProfiles/win-x86.pubxml
@@ -0,0 +1,14 @@
+
+
+
+
+ Release
+ AnyCPU
+ FileSystem
+ net10.0
+ win-x86
+ true
+ true
+ false
+
+
diff --git a/README.md b/README.md
index 2e30f99..fc5ca3e 100644
--- a/README.md
+++ b/README.md
@@ -25,9 +25,9 @@ Before you start, please ensure your device meets the following recommended requ
- **OS**: Windows x64 / Linux x64
- **CPU**: 1 Core or higher
-- **RAM**: 1GB or higher
+- **RAM**: 512M or higher
- **Disk**: 5GB available space
-- **Runtime**: [ASP.NET Core 9 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/9.0)
+- **Runtime**: Programs downloaded from Releases do not require .NET Runtime installation (built-in)
You can download and run the software from [Github Releases](https://github.com/sharwapi/sharwapi.core/releases).
diff --git a/README_CN.md b/README_CN.md
index 089941e..d00d7c2 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -25,9 +25,9 @@ SharwAPI (又称Sharw's API) 是一款基于.NET开发的模块化API框架,
- **系统**:Windows x64 / Linux x64
- **CPU**:1 核或更高
-- **内存**:1GB 或更高
+- **内存**:512M 或更高
- **硬盘**:5GB 可用空间
-- **运行时**:[ASP.NET Core 9 Runtime](https://dotnet.microsoft.com/zh-cn/download/dotnet/9.0)
+- **运行时**:从 Releases 下载的程序无需安装 .NET Runtime(程序已内置)
你可以在 [Github Releases](https://github.com/sharwapi/sharwapi.core/releases) 中下载软件并运行。
diff --git a/appsettings.json b/appsettings.json
index 5bb98af..f7b5ccd 100644
--- a/appsettings.json
+++ b/appsettings.json
@@ -1,13 +1,39 @@
{
- "Logging": {
- "LogLevel": {
+ "Serilog": {
+ "MinimumLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
+ "Override": {
+ "Microsoft.AspNetCore": "Information",
+ "System": "Warning"
+ }
+ },
+ "WriteTo": [
+ { "Name": "Console" },
+ {
+ "Name": "Async",
+ "Args": {
+ "configure": [
+ {
+ "Name": "File",
+ "Args": {
+ "path": "logs/log-.txt",
+ "rollingInterval": "Day",
+ "retainedFileCountLimit": 30,
+ "fileSizeLimitBytes": 10485760,
+ "rollOnFileSizeLimit": true
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "Enrich": [ "FromLogContext" ]
},
"Urls": "http://localhost:5000",
"ApiInfo": {
"Name": "Sharw's API",
"Version": "0.1.0"
+ },
+ "RouteOverride": {
}
}
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..e6e85ba
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sharwapi.Core.csproj b/sharwapi.Core.csproj
index ba5ea54..e7b5d58 100644
--- a/sharwapi.Core.csproj
+++ b/sharwapi.Core.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net10.0
enable
enable
4cd6e719-cb4f-4dbe-8f8a-5aef3154b923
@@ -9,11 +9,17 @@
+
+
+
+
+
-
+
+