当前位置: 首页 > news >正文

一个好用的模块化和自动服务注册框架

始顺序实例初始化所有模块类。

首先扫码模块所在的类型,把所有模块的类型扫描后,扫描每个类型时,都会出发模块所在的类型过滤器。

快速入手

创建 Demo1.Api、Demo1.Application 两个项目,在 Demo1.Application 在中引入最新的 Maomi.Core 包。

每个项目都应该有一个模块类,分别创建 ApplicationModule.cs、ApiModule.cs,模块类需要实现 IModule 接口。

Demo1.Application 项目的 ApplicationModule.cs 文件内容如下,其构造函数注入了 IConfiguration,模块类中可以使用依赖注入,可以注入一些 WebApplicationBuilder 默认注册的服务。

public class ApplicationModule : IModule { // 模块类中可以使用依赖注入 private readonly IConfiguration _configuration; public ApplicationModule(IConfiguration configuration) { _configuration = configuration; } public void ConfigureServices(ServiceContext services) { // 这里可以编写模块初始化代码 } }

或者什么都不注入:

public class ApplicationModule : IModule { public void ConfigureServices(ServiceContext services) { // 这里可以编写模块初始化代码 } }

在 Demo1.Application 项目里,如果需要将 MyService 注册到容器中,在类型上加上[InjectOnScoped]特性即可。

public interface IMyService { int Sum(int a, int b); } [InjectOnScoped] // 自动注册的标记 public class MyService : IMyService { public int Sum(int a, int b) { return a + b; } }

等同于:

service.AddScoped<IMyService, MyService>();

上层模块 Demo1.Api 中的 ApiModule.cs 可以通过特性注解引用底层模块。

using System.Reflection; [InjectModule<ApplicationModule>] // 指明依赖了 ApplicationModule 模块 public class ApiModule : IModule { public void ConfigureServices(ServiceContext services) { // 这里可以编写模块初始化代码 } }

最后,在程序启动时配置模块入口,并进行初始化。

var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // 注册模块化服务,并设置 ApiModule 为入口 builder.Services.AddModule<ApiModule>(); var app = builder.Build();

模块

Maomi.Core 也支持从程序集中弱引用模块类,即不直接调用模块类,而是动态扫描识别出模块类。

模块使用依赖注入

每个模块都需要实现 IModule 接口,其定义如下:

如果是单纯模型类模块或者纯接口抽象模块,则是没必要为此程序集添加模块。

/// <summary> /// 模块接口. /// </summary> public interface IModule { /// <summary> /// 模块中的依赖注入. /// </summary> /// <param name="context">模块服务上下文.</param> void ConfigureServices(ServiceContext context); }

在 ASP.NET Core 配置 Host 时,会自动注入一些框架依赖的服务,如 IConfiguration 等,因此在 开始初始化模块服务时,模块类的构造函数可以获取到已经注入的服务。

可以在模块类的构造函数注入想要的服务。

[InjectModule<ApplicationModule>] public class ApiModule : IModule { private readonly IConfiguration _configuration; private readonly IHostEnvironment _hostEnvironment; public ApiModule(IConfiguration configuration, IHostEnvironment hostEnvironment) { _configuration = configuration; _hostEnvironment = hostEnvironment; } public void ConfigureServices(ServiceContext context) { var configuration = context.Configuration; context.Services.AddCors(); } }

除了可以直接在模块构造函数注入服务之外,还可以通过ServiceContext context获取服务和配置。

/// <summary> /// 模块上下文. /// </summary> public abstract class ServiceContext { protected readonly IServiceCollection _serviceCollection; protected readonly IConfiguration _configuration; protected readonly List<ModuleRecord> _modules; /// <summary> /// Initializes a new instance of the <see cref="ServiceContext"/> class. /// </summary> /// <param name="serviceCollection"></param> /// <param name="configuration"></param> internal ServiceContext(IServiceCollection serviceCollection, IConfiguration configuration) { _serviceCollection = serviceCollection; _configuration = configuration; _modules = new List<ModuleRecord>(); } /// <summary> /// 容器服务集合. /// </summary> public IServiceCollection Services => _serviceCollection; /// <summary> /// 配置. /// </summary> public IConfiguration Configuration => _configuration; /// <summary> /// 已识别到的模块列表. /// </summary> public IReadOnlyList<ModuleRecord> Modules => _modules; }

例如,使用context.Services可以手动注册服务到容器中,使用context.Modules可以获取模块和程序集的相关信息。

context.Modules里面只记录了跟当前项目关联的模块类所在的程序集,可以避免在使用不同的框架时重复扫描项目所有的程序集。

public void ConfigureServices(ServiceContext context) { var configuration = context.Configuration; context.Services.AddCors(); context.Services.AddScoped<IMyService, MyService>(); // 注册 CQRS 服务. context.Services.AddMediatR(cfg => { cfg.MaxTypesClosing = 500; cfg.AddOpenBehavior(typeof(TraceBehavior<,>)); cfg.RegisterServicesFromAssemblies(context.Modules.Select(x => x.Assembly).ToArray()); }); }

例如,使用 MediatR 框架需要添加程序集,使用 AutoMapper 也需要添加程序集,使用AppDomain.CurrentDomain.GetAssemblies()会将非常多的程序集一起添加进去,由于 MediatR、AutoMapper 等框架会反射扫描所有程序集,导致项目启动时会有点慢。

使用context.Modules则只会注册有用的模块类所在的程序集,例如 上面的 Demo1 解决方案中,context.Modules里面只有Demo1.ApplicationDemo1.Api,可以避免引入其它程序集,只关注当前项目的程序集。

ModuleCore 抽象类

ModuleCore 是一个实现了 IModule 接口的抽象类,主要是多了一个 TypeFilter 方法,在扫描程序集的类型时会调用该方法,开发者可以通过此方法灵活处理一些类型。

/// <summary> /// 模块过滤器接口. /// </summary> public abstract class ModuleCore : IModule { /// <inheritdoc/> public abstract void ConfigureServices(ServiceContext context); /// <summary> /// 扫描每个类型时会调用该接口. /// </summary> /// <param name="type"></param> public abstract void TypeFilter(Type type); }

如下所示,框架扫描到一个类型时,触发了 TypeFilter 函数,开发者可以识别该类型,然后进行相应的处理。

public void TypeFilter(IServiceCollection services, Type type) { if (type.IsClass && !type.IsAbstract) { if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ITypeConverter<,>)) { services.AddScoped(type); } } }

项目里面可能会有很多第三方的框架以及自己编写的组件,如果每个框架都把所有程序集的类型都扫描一遍,会导致启动耗时变大,而且这个过程也是重复的,通过 TypeFilter 可以减少重复扫描过程。

模块类都可以自由继承 ModuleCore 或者 IModule。

例如,在项目中,开发者自己实现了一个事件总线的框架组件,以及一个自动对象映射的框架组件,将这两个组件封装成两个模块,由于它们关注的类型对象不一样,所以每个模块对当前扫描到的类型处理方法不一样,比如说识别特性注解、识别接口,然后做对应的处理逻辑。

如下代码所示,不同的逻辑分别在不同的模块类:

public class EventBusModule : ModuleCore { public override void ConfigureServices(ServiceContext context) { } public override void TypeFilter(IServiceCollection services, Type type) { // } }
public class AutoMapperModule : ModuleCore { public override void ConfigureServices(IServiceCollection services, ServiceContext context) { } public override void TypeFilter(Type type) { } }

自定义模块配置

在使用AddModule()时可以注入 ModuleOptions 配置,影响模块化行为,ModuleOptions 定义如下:

/// <summary> /// 初始化配置. /// </summary> public class ModuleOptions { /// <summary> /// 注册服务时要过滤的类型或接口,这些类型不会被注册到容器中. /// </summary> public ICollection<Type> FilterServiceTypes { get; } { typeof(IDisposable), typeof(ICloneable), typeof(IComparable), typeof(object) }; /// <summary> /// 自定义要注册的程序集. /// </summary> public ICollection<Assembly> CustomAssembies { get; } }

在自动服务注册时,框架会自动忽略把 IDisposable、ICloneable 这里没意义的接口注册到容器中,读者也可以添加一些过滤接口。

例如对于 MyService 服务,实现了 IMyService、IDisposable 两个接口。

public interface IMyService { int Sum(int a, int b); } [InjectOnScoped] public class MyService : IMyService, IDisposable { public int Sum(int a, int b) { return a + b; } public void Dispose() { throw new NotImplementedException(); } }

由于默认过滤规则,最终只会注册:

context.Services.AddScoped<IMyService, MyService>();

而不会注册成:

context.Services.AddScoped<IMyService, MyService>(); context.Services.AddScoped<IDisposable, MyService>();

如果开发者需要动态引入程序集,不使用模块类强引用时,可以使用CustomAssembies

builder.Services.AddModule<ApiModule>(options => { options.CustomAssembies.Add(Assembly.Load("./aaa.dll")); });

aaa.dll 里面需要有模块类。

模块加载

在项目启动时,模块加载的流程如下:

  1. 识别模块依赖树。
  2. 按照模块依赖树初始化各个模块类。
  3. 初始化自定义程序集模块类。
  4. 按照模块依赖树扫描程序集中的类型,调用各个模块类的TypeFilter函数。
  5. 顺序扫描自定义程序集模块的类型,调用各个模块类的TypeFilter函数。

如果一个 B 模块需要依赖另一个 A 模块,或者 A 模块必须先在 B 模块之前初始化,那么可以在 B 模块上使用[InjectModule]引入 A 模块,这种规则称为模块依赖树。

如下代码所示,由于 B 依赖了 A 模块,因此会先初始化 A 模块之后才会初始化 B 模块。

class A:IModule [InjectModule<A>()] class B:IModule

关于模块依赖的规则,后面的小节会更加详细讲解。

对于程序集引用的模块,即使用了[InjectModule],框架也会忽略依赖关系,只会直接加载当前模块类,不会构建模块依赖树。

如下所示,即使aaa.dll中的模块类使用[InjectModule]引入了其它模块类,框架会忽略这种关系。

builder.Services.AddModule<ApiModule>(options => { options.CustomAssembies.Add(Assembly.Load("./aaa.dll")); });

循环依赖检测

因为模块之间会有依赖关系,为了识别这些依赖关系,Maomi.Core 使用树来表达依赖关系。Maomi.Core 在启动模块服务时,扫描所有模块类,然后将模块依赖关系存放到模块树中,然后按照左序遍历的算法对模块逐个初始化,也就是先从底层模块开始进行初始化。

Maomi.Core 可以识别模块循环依赖,比如,有以下模块和依赖出现循环,那么将会抛出错误。

[InjectModule<A>()] [InjectModule<B>()] class C:IModule [InjectModule<A>()] class B:IModule // 这里出现了循环依赖 [InjectModule<C>()] class A:IModule // C 是入口模块 services.AddModule<C>();

因为 C 模块依赖 A、B 模块,所以 A、B 是节点 C 的子节点,而 A、B 的父节点则是 C。当把 A、B、C 三个模块以及依赖关系扫描完毕之后,会得到以下的模块依赖树。

如下图所示,每个模块都做了下标,表示不同的依赖关系,一个模块可以出现多次,C1 -> A0表示 C 依赖 A。

C0 开始,没有父节点,则不存在循环依赖。

从 A0 开始,A0 -> C0 ,该链路中也没有出现重复的 A 模块。

从 C1 开始,C1 -> A0 -> C0 ,该链路中 C 模块重复出现,则说明出现了循环依赖。

从 C2 开始,C2 -> A1 -> B0 -> C0 ,该链路中 C 模块重复出现,则说明出现了循环依赖。

模块初始化顺序

在生成模块树之后,通过对模块树进行后序遍历,可以保证正确的模块初始化顺序。

比如,有以下模块以及依赖。

[InjectModule<C>()] [InjectModule<D>()] class E:IModule [InjectModule<A>()] [InjectModule<B>()] class C:IModule [InjectModule<B>()] class D:IModule [InjectModule<A>()] class B:IModule class A:IModule // E 是入口模块 services.AddModule<E>();

生成模块依赖树如图所示:

http://www.jsqmd.com/news/560635/

相关文章:

  • 天津灵活用工服务怎么选?天津政集企业管理有限公司深耕天津东丽区天津滨海新区等地合规专业口碑好 - 十大品牌榜
  • 从公差锁死到标准化维保:解析GT Show现场RF RACER的系统级结项 - RF_RACER
  • CLIP-GmP-ViT-L-14在.NET生态中的集成:使用C#调用跨模态模型服务
  • 保姆级教程:用ROS的ipa_room_exploration包实现清洁机器人全覆盖路径(附源码解析)
  • 融智天合同管理系统合同台账体验 - 业财科技
  • Cufflinks完全指南:如何用Python轻松创建专业级金融图表
  • 如何为Cache贡献代码:开源项目参与指南
  • 深入解析原生HTTP与MCP服务器的交互机制
  • 一键生成古风角色:圣女司幼幽-造相Z-Turbo镜像使用入门
  • 5步解锁全速下载:开源工具彻底解决网盘限速难题
  • DVWA-Chinese从入门到精通:Web安全实践平台完全指南
  • 如何通过 Firecrawl MCP Server 与 Windsurf 集成提升 AI 代码助手的网页理解能力
  • 7步打造坚不可摧的团队安全文化:从意识培训到实践落地
  • Pixel Mind Decoder 效果对比评测:在不同文体和语言风格下的表现
  • Gephi进阶指南——外观定制与布局优化实战
  • 伍德沃德9907-1199产品性能稳定,故障率低
  • 跨平台应用部署解决方案:如何安全高效地在Windows系统安装APK文件
  • 【AI】JSON 格式:执行式AI数据交互核心语法
  • OpenClaw+Qwen3-32B:个人知识库自动化更新方案
  • Cache单元测试完全手册:如何为缓存库编写高质量测试
  • 如何在5分钟内快速掌握BepInEx:Unity游戏插件框架的终极实用指南
  • 天津政集企业管理有限公司:众包服务商,深耕天津东丽区天津滨海新区等地区,赋能企业发展 - 十大品牌榜
  • 别再死磕MIG了!ZYNQ PS端DDR3做帧缓存,用VDMA+HP接口实战指南
  • FactoryIO机械手仿真取料程序-西门子1200仿真及软件安装包
  • Cadence启动文件背后的设计哲学:为什么.cdsinit总覆盖不了.cdsenv的设置?
  • # 用idea编写代码
  • 如何解决echarts-for-react常见问题:7个实用错误排查与修复技巧
  • AWPortrait-Z人像美化LoRA部署指南:WebUI一键安装,开箱即用
  • Vue3 TypeScript Element-Plus 企业级后台管理系统架构设计与实现
  • 终极指南:VSCode Rainbow Fart如何通过Vue.js打造沉浸式编程体验