ABP 模块系统源码学习:启动时模块是如何加载和排序的

从入口看起:AbpApplicationFactory.Create

csharp 复制代码
using var app = AbpApplicationFactory.Create<BusinessModule>(options =>
{
    options.UseAutofac();
});
app.Initialize();

这行代码的背后发生了什么?直接看源码 AbpApplicationFactory.cs

csharp 复制代码
public static IAbpApplicationWithInternalServiceProvider Create<TStartupModule>(
    Action<AbpApplicationCreationOptions>? optionsAction = null)
{
    return new AbpApplicationWithInternalServiceProvider(typeof(TStartupModule), optionsAction);
}

它创建了 AbpApplicationWithInternalServiceProvider,这个类在构造时做了两件关键的事:

  1. 创建 ModuleLoader 并调用 LoadModules 加载所有模块
  2. 按拓扑排序确定模块初始化顺序

深度解析 ModuleLoader.LoadModules

源码位于 framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleLoader.cs,核心逻辑只有三行:

csharp 复制代码
public IAbpModuleDescriptor[] LoadModules(IServiceCollection services, Type startupModuleType, PlugInSourceList plugInSources)
{
    var modules = GetDescriptors(services, startupModuleType, plugInSources);  // 1. 发现所有模块
    modules = SortByDependency(modules, startupModuleType);                     // 2. 拓扑排序
    return modules.ToArray();                                                    // 3. 返回有序列表
}

第一步:递归发现

GetDescriptors 内部分两步:

FillModules ------从入口模块开始,递归查找所有 [DependsOn] 依赖:

csharp 复制代码
// AbpModuleHelper.cs
private static void AddModuleAndDependenciesRecursively(List<Type> moduleTypes, Type moduleType, ...)
{
    moduleTypes.Add(moduleType);
    foreach (var dependedModuleType in FindDependedModuleTypes(moduleType))
    {
        AddModuleAndDependenciesRecursively(moduleTypes, dependedModuleType, ...);
    }
}

FindDependedModuleTypes 通过反射读取当前类上的所有 [DependsOn] 属性:

csharp 复制代码
public static List<Type> FindDependedModuleTypes(Type moduleType)
{
    var dependencyDescriptors = moduleType.GetCustomAttributes().OfType<IDependedTypesProvider>();
    foreach (var descriptor in dependencyDescriptors)
    {
        dependencies.AddIfNotContains(descriptor.GetDependedTypes());
    }
}

这意味着不仅 [DependsOn] 会被识别,任何实现了 IDependedTypesProvider 的自定义属性都能用来声明依赖。

第二步:拓扑排序

csharp 复制代码
protected virtual List<IAbpModuleDescriptor> SortByDependency(List<IAbpModuleDescriptor> modules, Type startupModuleType)
{
    var sortedModules = modules.SortByDependencies(m => m.Dependencies);
    sortedModules.MoveItem(m => m.Type == startupModuleType, modules.Count - 1);
    return sortedModules;
}

这里使用了 SortByDependencies 扩展方法(来自 Volo.Abp.Core 的工具类),是一个标准的拓扑排序实现。排序后,入口模块被移到最后一位,保证它最后初始化。

如果在模块依赖图中存在循环依赖,拓扑排序会抛出异常------这在启动时就能发现,不会让问题带到运行时。

第三步:创建模块实例

csharp 复制代码
protected virtual IAbpModule CreateAndRegisterModule(IServiceCollection services, Type moduleType)
{
    var module = (IAbpModule)Activator.CreateInstance(moduleType)!;
    services.AddSingleton(moduleType, module);  // 每个模块以 Singleton 注册到 DI
    return module;
}

每个模块实例被注册为 Singleton,这意味着模块在整个应用生命周期中只创建一次。

生命周期钩子的调用顺序

模块加载完毕后调用 Initialize,进入生命周期阶段。在 AbpApplicationBase.cs 中:

复制代码
阶段 1:ConfigureServices
  for each module in sortedModules:
    module.Instance.PreConfigureServices(context)
  for each module in sortedModules:
    module.Instance.ConfigureServices(context)  
  for each module in sortedModules:
    module.Instance.PostConfigureServices(context)

阶段 2:构建 IServiceProvider

阶段 3:OnApplicationInitialization  
  for each module in sortedModules:
    module.Instance.OnPreApplicationInitialization(context)
    module.Instance.OnApplicationInitialization(context)
    module.Instance.OnPostApplicationInitialization(context)

注意:三个阶段都按排序后的顺序执行,入口模块永远在最后。

实战要点

1. 模块不可循环依赖

如果模块 A 依赖 B,B 又依赖 A,启动时直接抛出 AbpException。这比运行时发现要好得多。

2. AdditionalAssembly 的用途

除了 [DependsOn],还有 [AdditionalAssembly] 属性。它不声明依赖,只是告诉框架"这个模块的程序集也加载进来"。用于解决模块类在另一个程序集中的场景。

3. 模块实例是 Singleton

模块中定义的状态在应用生命周期内保持。不要在模块中存储请求级别的数据。

4. PreConfigureServices 的应用

PreConfigureServices 可以用于设置默认选项,让其他模块在 ConfigureServices 中可以覆盖:

csharp 复制代码
public override void PreConfigureServices(ServiceConfigurationContext context)
{
    // 设置默认值,允许下游模块覆盖
    context.Services.AddSingleton(new DefaultOptions { ... });
}

验证结果

运行练习示例,输出清晰展示了顺序:

复制代码
[LoggingModule] ConfigureServices          ← 被依赖的模块先执行
[DataAccessModule] ConfigureServices
[BusinessModule] ConfigureServices         ← 入口模块最后执行

即使打乱 [DependsOn] 的书写顺序,输出顺序不变。原因就是 SortByDependencies 的拓扑排序保证了依赖顺序,和代码中的书写顺序无关。

相关推荐
硅基喵3 个月前
C# 也能像 Python 一样写脚本 | .NET 10 构建基于文件的应用
dotnet
硅基喵3 个月前
.NET 10 使用 Microsoft.AspNetCore.OpenApi 实现 API 版本管理
dotnet
硅基喵4 个月前
ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑
dotnet
ChaITSimpleLove5 个月前
aiagent-webapi 命令的详细使用说明
dotnet·webapi·ai agent·agent framework·maf·projecttemp
TeamDev5 个月前
使用 Docker 部署 DotNetBrowser 应用程序
运维·ui·docker·容器·桌面应用·dotnet·dotnetbrowser
CSharp精选营5 个月前
.NET命名之谜:它与C#纠缠20年的关系揭秘
c#·.net·dotnet·csharp
VAllen5 个月前
ConcurrentNativeQueue<T>:一个使用 .NET 实现的零 GC 压力的无锁 MPSC 原生队列
c#·.net·性能测试·.net core·dotnet·csharp
lindexi6 个月前
dotnet Vortice 通过 Angle 将 Skia 和 DirectX 对接
dotnet
初级代码游戏6 个月前
C#:程序发布的大小控制 裁剪 压缩
c#·.net·dotnet·压缩·大小·发布·裁剪