修改 Startup 加载 和Module 加载

This commit is contained in:
chfei committed 2026-09-23 18:47:54 +08:00
1 parent c732ecb1d2
commit c7fb447034
17 files changed
+462 -383

No files matched your search

@@ -1,125 +1,123 @@
using MAX.Framework.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
//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();
//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 AddStore(this WebApplicationBuilder builder)
// {
// IEnumerable<Assembly> assemblies = ServiceCollectionExtensions.GetApplicationAssemblies();
// 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证后面两个阶段拿到的是同一个对象
Dictionary<Type, object> instances = [];
ModuleRegistry modules = ServiceCollectionExtensions.DiscoverModules(assemblies, instances);
// // 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证后面两个阶段拿到的是同一个对象
// Dictionary<Type, object> instances = [];
// ModuleRegistry modules = ServiceCollectionExtensions.DiscoverModules(assemblies, instances);
// 必须在跑 AddModule 之前并入,模块注册时才读得到自己的配置节
foreach (IModule module in modules.Modules)
{
AddModuleConfigurationFile(builder.Configuration, module);
}
// // 必须在跑 AddModule 之前并入,模块注册时才读得到自己的配置节
// foreach (IModule module in modules.Modules)
// {
// AddModuleConfigurationFile(builder.Configuration, module);
// }
builder.Services.AddStoreHubCore(assemblies, modules, instances, builder.Configuration, builder.Environment);
return builder;
}
// 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;
}
// /// <summary>
// /// 执行所有模块的管道阶段(<see cref="IModule.UseModule"/>):注册中间件、看板、Map 端点等。
// /// </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);
}
}
// foreach (IModule module in registry.Modules)
// {
// try
// {
// module.UseModule(app);
// }
// catch (Exception ex)
// {
// throw new InvalidOperationException($"模块 {module.GetType().FullName} 的 UseModule 执行失败。", ex);
// }
// }
return app;
}
// 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;
}
// /// <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);
// string path = Path.IsPathRooted(fileName)
// ? fileName
// : Path.Combine(AppContext.BaseDirectory, fileName);
JsonConfigurationSource source = new()
{
Path = path,
Optional = true, // 文件不存在时不报错(模块没配就是没有)
ReloadOnChange = true // 允许改配置不重启(与 appsettings.json 行为一致)
};
source.ResolveFileProvider();
// JsonConfigurationSource source = new()
// {
// Path = path,
// Optional = true, // 文件不存在时不报错(模块没配就是没有)
// ReloadOnChange = true // 允许改配置不重启(与 appsettings.json 行为一致)
// };
// source.ResolveFileProvider();
IList<IConfigurationSource> sources = configuration.Sources;
int index = sources.Count;
// 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;
}
}
// // 找到最后一个 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);
// sources.Insert(index, source);
if (configuration is IConfigurationRoot root)
{
root.Reload();
}
}
}
}
// if (configuration is IConfigurationRoot root)
// {
// root.Reload();
// }
// }
// }
//}
@@ -1,20 +0,0 @@
using MAX.Framework.Interfaces;
namespace MAX.Framework.DependencyInjection
{
/// <summary>
/// 模块注册表:保存 <c>AddStoreHub()</c> 阶段发现(并按优先级排序)的模块实例。
/// 它被注册进容器,好让 <c>app.UseStoreHubModules()</c> 在 <c>builder.Build()</c> 之后拿到
/// 「同一批实例」继续执行管道阶段,而不是重新反射创建一遍。
/// </summary>
internal sealed class ModuleRegistry
{
public ModuleRegistry(IReadOnlyList<IModule> modules)
{
Modules = modules;
}
/// <summary>按优先级降序排列的模块实例</summary>
public IReadOnlyList<IModule> Modules { get; }
}
}
@@ -3,10 +3,13 @@ using MAX.Core.Interfaces;
using MAX.Framework.Commons;
using MAX.Framework.Dtos;
using MAX.Framework.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Win32;
using System.Configuration;
using System.Diagnostics;
using System.Reflection;
@@ -25,70 +28,37 @@ namespace MAX.Framework.DependencyInjection
];
/// <summary>
/// 兼容入口:只有服务集合,没有配置与环境(模块拿到的 <see cref="ModuleContext.Configuration"/> 是空配置),
/// 也拿不到 IConfigurationBuilder,所以模块自己的配置文件不会加载。新代码请用 <c>builder.AddStoreHub()</c>。
/// 添加框架注册
/// </summary>
public static IServiceCollection AddStoreHub(this IServiceCollection services)
{
return services.AddStoreHub(null, null);
}
/// <summary>
/// 带配置的注册入口:<c>builder.Services.AddStoreHub(builder.Configuration)</c>
/// </summary>
public static IServiceCollection AddStoreHub(this IServiceCollection services, IConfiguration? configuration)
{
return services.AddStoreHub(configuration, null);
}
/// <summary>
/// 注册应用服务:
/// <list type="number">
/// <item>自动注册所有实现 <see cref="ISingletonDependency"/> / <see cref="IScopedDependency"/> / <see cref="ITransientDependency"/> 的类型;</item>
/// <item>发现所有 <see cref="IStartup"/>,按 <see cref="IStartup.Priority"/> 降序执行 Initialize + Register;</item>
/// <item>发现所有 <see cref="IModule"/>,按 <see cref="IModule.Priority"/> 降序执行 <see cref="IModule.AddModule"/>,
/// 并把模块实例登记进容器,留给 <c>app.UseStoreHubModules()</c> 执行管道阶段。</item>
/// </list>
/// 这个入口不加载模块自己的配置文件,需要模块配置请用 <c>builder.AddStoreHub()</c>。
/// </summary>
public static IServiceCollection AddStoreHub(this IServiceCollection services, IConfiguration? configuration, IHostEnvironment? environment)
public static IServiceCollection AddStore(this IHostApplicationBuilder host)
{
IEnumerable<Assembly> assemblies = GetApplicationAssemblies();
// 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证两个阶段拿到的是同一个对象
Dictionary<Type, object> instances = [];
ModuleRegistry modules = DiscoverModules(assemblies, instances);
return services.AddStoreHubCore(assemblies, modules, instances, configuration, environment);
return AddStoreCore(host, assemblies, instances);
}
/// <summary>
/// 注册应用服务的公共实现:宿主入口(<c>builder.AddStoreHub()</c>)与 <see cref="IServiceCollection"/> 入口共用。
/// </summary>
internal static IServiceCollection AddStoreHubCore(
this IServiceCollection services,
internal static IServiceCollection AddStoreCore(
IHostApplicationBuilder host,
IEnumerable<Assembly> assemblies,
ModuleRegistry modules,
IDictionary<Type, object> instances,
IConfiguration? configuration,
IHostEnvironment? environment)
IDictionary<Type, object> instances)
{
RegisterDependencyMarkers(services, assemblies);
RunStartups(services, assemblies, instances);
RegisterDependencyMarkers(host.Services, assemblies);
ModuleContext context = new(
services,
new ServiceRegistrarProxy(services),
configuration ?? new ConfigurationBuilder().Build(),
environment);
RunModules(host, assemblies, instances);
RunModules(modules, context);
// 注册表必须在 Build() 之前进容器,UseStoreHubModules() 才拿得到
services.AddSingleton(modules);
RunStartups(host.Services, assemblies, instances);
services.AddControllers(options =>
host.Services.AddControllers(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
options.Filters.Add<UnifyResultFilter>();
@@ -113,7 +83,7 @@ namespace MAX.Framework.DependencyInjection
};
});
return services;
return host.Services;
}
/// <summary>
@@ -149,39 +119,33 @@ namespace MAX.Framework.DependencyInjection
}
}
}
private static IEnumerable<IStartup> Startups = [];
/// <summary>
/// 发现所有 <see cref="IStartup"/> 实现,按 <see cref="IStartup.Priority"/> 降序执行
/// </summary>
private static void RunStartups(IServiceCollection services, IEnumerable<Assembly> assemblies, IDictionary<Type, object> instances)
{
IServiceRegistrar registrar = new ServiceRegistrarProxy(services);
foreach (IStartup startup in CreateInstances<IStartup>(assemblies, instances).OrderByDescending(s => s.Priority))
Startups = CreateInstances<IStartup>(assemblies, instances).OrderByDescending(s => s.Priority);
foreach (IStartup startup in Startups)
{
startup.Initialize();
startup.Register(registrar);
}
}
/// <summary>
/// 发现所有 <see cref="IModule"/> 实现,只创建实例,不执行任何阶段
/// </summary>
internal static ModuleRegistry DiscoverModules(IEnumerable<Assembly> assemblies, IDictionary<Type, object> instances)
{
return new ModuleRegistry([.. CreateInstances<IModule>(assemblies, instances).OrderByDescending(m => m.Priority)]);
}
private static IEnumerable<IModule> Modules = [];
/// <summary>
/// 执行所有模块的服务注册阶段(<see cref="IModule.AddModule"/>)
/// </summary>
private static void RunModules(ModuleRegistry modules, ModuleContext context)
private static void RunModules(IHostApplicationBuilder host, IEnumerable<Assembly> assemblies, IDictionary<Type, object> instances)
{
foreach (IModule module in modules.Modules)
Modules = CreateInstances<IModule>(assemblies, instances).OrderByDescending(s => s.Priority);
foreach (IModule module in Modules)
{
try
{
module.AddModule(context);
module.AddModule(host);
}
catch (Exception ex)
{
@@ -234,7 +198,7 @@ namespace MAX.Framework.DependencyInjection
internal static IEnumerable<Assembly> GetApplicationAssemblies()
{
// 使用 AppTypeFinder:扫描应用输出目录 + 已加载程序集,
return StoreHubAppTypeFinder.Current.GetAssemblies();
return StoreAppTypeFinder.Current.GetAssemblies();
}
private static bool IsSystemInterface(Type type)
@@ -244,5 +208,20 @@ namespace MAX.Framework.DependencyInjection
|| type.Namespace.StartsWith("Microsoft", StringComparison.Ordinal);
}
public static IApplicationBuilder UseStore(this IApplicationBuilder app)
{
foreach (IStartup startup in Startups)
{
startup.Start((app as IHost).Services);
}
foreach (IModule module in Modules)
{
module.UseModule(app);
}
return app;
}
}
}
@@ -6,9 +6,9 @@ namespace MAX.Framework.DependencyInjection
/// 本应用的类型查找器:在 AppTypeFinder 默认前缀(MAX. / Nexus.)之外,
/// 增加 StoreHub、KJD 前缀,用于发现宿主和各业务模块的程序集。
/// </summary>
internal sealed class StoreHubAppTypeFinder : AppTypeFinder
internal sealed class StoreAppTypeFinder : AppTypeFinder
{
public static StoreHubAppTypeFinder Current { get; } = new();
public static StoreAppTypeFinder Current { get; } = new();
protected override List<string> AvailablePrefix()
{