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 @@ + + + + + - + +