Files
StoreWeb/Core/MAX.Framework/DependencyInjection/HostBuilderExtensions.cs
T
2026-09-18 19:22:49 +08:00

126 lines
4.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 宿主(Program.cs)入口:模块两阶段装配 + 模块自己的配置文件。
/// <code>
/// var builder = WebApplication.CreateBuilder(args);
/// builder.AddStoreHub(); // 阶段一:加载各模块配置文件 + 自动执行所有 IModule.AddModule
///
/// var app = builder.Build();
/// // ... 宿主自己的中间件、认证、端点 ...
/// app.UseStoreHubModules(); // 阶段二:管道装配(自动执行所有 IModule.UseModule)
/// app.Run();
/// </code>
/// </summary>
public static class HostBuilderExtensions
{
/// <summary>
/// 在 <see cref="WebApplicationBuilder"/> 上注册应用框架服务:
/// <list type="number">
/// <item>发现所有 <see cref="IModule"/>;</item>
/// <item>把每个模块声明的 <see cref="IModule.ConfigurationFile"/> 并入宿主配置树(宿主 appsettings.json 不需要放模块的配置);</item>
/// <item>再执行 <c>builder.Services.AddStoreHub(builder.Configuration, builder.Environment)</c>。</item>
/// </list>
/// </summary>
public static WebApplicationBuilder AddStoreHub(this WebApplicationBuilder builder)
{
IEnumerable<Assembly> assemblies = ServiceCollectionExtensions.GetApplicationAssemblies();
// 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证后面两个阶段拿到的是同一个对象
Dictionary<Type, object> 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;
}
/// <summary>
/// 执行所有模块的管道阶段(<see cref="IModule.UseModule"/>):注册中间件、看板、Map 端点等。
/// 必须在 <c>builder.Build()</c> 之后、<c>app.Run()</c> 之前调用;
/// 建议放在 <c>UseAuthorization()</c> 之后、<c>MapControllers()</c> 之前。
/// </summary>
public static WebApplication UseStoreHubModules(this WebApplication app)
{
// 宿主没调用过 AddStoreHub() 时容器里没有注册表,直接返回即可
ModuleRegistry? registry = app.Services.GetService<ModuleRegistry>();
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;
}
/// <summary>
/// 把模块自己的配置文件并进宿主配置:插在 appsettings*.json 之后、环境变量/命令行之前,
/// 这样模块配置能覆盖宿主 appsettings.json,同时仍然允许用环境变量/命令行覆盖它。
/// </summary>
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<IConfigurationSource> 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();
}
}
}
}