using MAX.Framework.Interfaces; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.DependencyInjection; using System.Reflection; namespace MAX.Framework.DependencyInjection { /// /// 宿主(Program.cs)入口:模块两阶段装配 + 模块自己的配置文件。 /// /// var builder = WebApplication.CreateBuilder(args); /// builder.AddStoreHub(); // 阶段一:加载各模块配置文件 + 自动执行所有 IModule.AddModule /// /// var app = builder.Build(); /// // ... 宿主自己的中间件、认证、端点 ... /// app.UseStoreHubModules(); // 阶段二:管道装配(自动执行所有 IModule.UseModule) /// app.Run(); /// /// public static class HostBuilderExtensions { /// /// 在 上注册应用框架服务: /// /// 发现所有 ; /// 把每个模块声明的 并入宿主配置树(宿主 appsettings.json 不需要放模块的配置); /// 再执行 builder.Services.AddStoreHub(builder.Configuration, builder.Environment)。 /// /// public static WebApplicationBuilder AddStoreHub(this WebApplicationBuilder builder) { IEnumerable assemblies = ServiceCollectionExtensions.GetApplicationAssemblies(); // 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证后面两个阶段拿到的是同一个对象 Dictionary instances = []; ModuleRegistry modules = ServiceCollectionExtensions.DiscoverModules(assemblies, instances); // 必须在跑 AddModule 之前并入,模块注册时才读得到自己的配置节 foreach (IModule module in modules.Modules) { AddModuleConfigurationFile(builder.Configuration, module); } builder.Services.AddStoreHubCore(assemblies, modules, instances, builder.Configuration, builder.Environment); return builder; } /// /// 执行所有模块的管道阶段():注册中间件、看板、Map 端点等。 /// 必须在 builder.Build() 之后、app.Run() 之前调用; /// 建议放在 UseAuthorization() 之后、MapControllers() 之前。 /// public static WebApplication UseStoreHubModules(this WebApplication app) { // 宿主没调用过 AddStoreHub() 时容器里没有注册表,直接返回即可 ModuleRegistry? registry = app.Services.GetService(); if (registry is null) { return app; } foreach (IModule module in registry.Modules) { try { module.UseModule(app); } catch (Exception ex) { throw new InvalidOperationException($"模块 {module.GetType().FullName} 的 UseModule 执行失败。", ex); } } return app; } /// /// 把模块自己的配置文件并进宿主配置:插在 appsettings*.json 之后、环境变量/命令行之前, /// 这样模块配置能覆盖宿主 appsettings.json,同时仍然允许用环境变量/命令行覆盖它。 /// private static void AddModuleConfigurationFile(IConfigurationBuilder configuration, IModule module) { string? fileName = module.ConfigurationFile; if (string.IsNullOrWhiteSpace(fileName)) { return; } string path = Path.IsPathRooted(fileName) ? fileName : Path.Combine(AppContext.BaseDirectory, fileName); JsonConfigurationSource source = new() { Path = path, Optional = true, // 文件不存在时不报错(模块没配就是没有) ReloadOnChange = true // 允许改配置不重启(与 appsettings.json 行为一致) }; source.ResolveFileProvider(); IList sources = configuration.Sources; int index = sources.Count; // 找到最后一个 appsettings*.json,插到它后面 for (int i = sources.Count - 1; i >= 0; i--) { if (sources[i] is FileConfigurationSource file && Path.GetFileName(file.Path).StartsWith("appsettings", StringComparison.OrdinalIgnoreCase)) { index = i + 1; break; } } sources.Insert(index, source); if (configuration is IConfigurationRoot root) { root.Reload(); } } } }