模块化和自动服务注册
基于 ASP.NET Core 开发的 Web 框架中,最著名的是 ABP,ABP 主要特点之一开发不同项目(程序集)时,在每个项目中创建一个模块类,程序加载每个程序集中,扫描出所有的模块类,然后通过模块类作为入口,初始化程序集。
使用模块化开发程序,好处是不需要关注程序集如何加载配置。开发人员开发程序集时,在模块类中配置如何初始化、如何读取配置,使用者只需要将模块类引入进来即可,由框架自动启动模块类。
Maomi.Core 也提供了模块化开发的能力,同时还包括简单易用的自动服务注册。Maomi.Core 是一个很简洁的包,可以在控制台、Web 项目、WPF 项目中使用,在 WPF 项目中结合 MVVM 可以大量减少代码复杂度,让代码更加清晰明朗。
快速入手
有 Demo1.Api、Demo1.Application 两个项目,每个项目都有一个模块类,模块类需要实现 IModule 接口。
Demo1.Application 项目的 ApplicationModule.cs 文件内容如下:
public class ApplicationModule : IModule
{
// 模块类中可以使用依赖注入
private readonly IConfiguration _configuration;
public ApplicationModule(IConfiguration configuration)
{
_configuration = configuration;
}
public void ConfigureServices(ServiceContext services)
{
// 这里可以编写模块初始化代码
}
}
复制代码
如果要将服务注册到容器中,在 class 上加上 [InjectOn]
特性即可。
public interface IMyService
{
int Sum(int a, int b);
}
[InjectOn] // 自动注册的标记
public class MyService : IMyService
{
public int Sum(int a, int b)
{
return a + b;
}
}
复制代码
上层模块 Demo1.Api 中的 ApiModule.cs 可以通过特性注解引用底层模块。
[InjectModule<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();
复制代码
模块可以依赖注入
在 ASP.NET Core 配置 Host 时,会自动注入一些框架依赖的服务,如 IConfiguration 等,因此在 .AddModule<ApiModule>()
开始初始化模块服务时,模块获取已经注入的服务。
每个模块都需要实现 IModule 接口,其定义如下:
/// <summary>
/// 模块接口
/// </summary>
public interface IModule
{
/// <summary>
/// 模块中的依赖注入
/// </summary>
/// <param name="context">模块服务上下文</param>
void ConfigureServices(ServiceContext context);
}
复制代码
除了可以直接在模块构造函数注入服务之外,还可以通过 ServiceContext context
获取服务和配置。
/// <summary>
/// 模块上下文
/// </summary>
public class ServiceContext
{
private readonly IServiceCollection _serviceCollection;
private readonly IConfiguration _configuration;
internal ServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
{
_serviceCollection = serviceCollection;
_configuration = configuration;
}
/// <summary>
/// 依赖注入服务
/// </summary>
public IServiceCollection Services => _serviceCollection;
/// <summary>
/// 配置
/// </summary>
public IConfiguration Configuration => _configuration;
}
复制代码
模块化
因为模块之间会有依赖关系,为了识别这些依赖关系,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>();
复制代码
生成模块依赖树如图所示:
首先从 E0 开始扫描,因为 E0 下存在子节点 C0、 D0,那么就会先顺着 C0 再次扫描,扫描到 A0 时,因为 A0 下已经没有子节点了,所以会对 A0 对应的模块 A 进行初始化。根据上图模块依赖树进行后序遍历,初始化模块的顺序是(已经被初始化的模块会跳过):
服务自动注册
Maomi.Core 是通过 [InjectOn]
识别要注册该服务到容器中,其定义如下:
/// <summary>
/// 依赖注入标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class InjectOnAttribute : Attribute
{
/// <summary>
/// 要注入的服务
/// </summary>
public Type[]? ServicesType { get; set; }
/// <summary>
/// 生命周期
/// </summary>
public ServiceLifetime Lifetime { get; set; }
/// <summary>
/// 注入模式
/// </summary>
public InjectScheme Scheme { get; set; }
/// <summary>
/// 是否注入自己
/// </summary>
public bool Own { get; set; } = false;
/// <summary>
///
/// </summary>
/// <param name="lifetime"></param>
/// <param name="scheme"></param>
public InjectOnAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient, InjectScheme scheme = InjectScheme.OnlyInterfaces)
{
Lifetime = lifetime;
Scheme = scheme;
}
}
复制代码
使用 [InjectOn]
时,默认是注册服务为 Transient
生命周期,且注册所有接口。
[InjectOn]
public class MyService : IAService, IBService
复制代码
等同于:
services.AddTransient<IAService, MyService>();
services.AddTransient<IBService, MyService>();
复制代码
如果只想注册 IAService
,可以将注册模式设置为InjectScheme.Some
,然后自定义注册的类型:
[InjectOn(
lifetime: ServiceLifetime.Transient,
Scheme = InjectScheme.Some,
ServicesType = new Type[] { typeof(IAService) }
)]
public class MyService : IAService, IBService
复制代码
也可以把自身注册到容器中:
[InjectOn(Own = true)]
public class MyService : IMyService
复制代码
等同于:
services.AddTransient<IAService, MyService>();
services.AddTransient<MyService>();
复制代码
如果服务继承了类、接口,只想注册父类,那么可以这样写:
public class ParentService { }
[InjectOn(
Scheme = InjectScheme.OnlyBaseClass
)]
public class MyService : ParentService, IDisposable
复制代码
等同于:
services.AddTransient<ParentService, MyService>();
services.AddTransient<MyService>();
复制代码
如果只注册自身,忽略接口等,可以使用:
[InjectOn(ServiceLifetime.Scoped, Scheme = InjectScheme.None, Own = true)]
复制代码
模块化和自动服务注册的设计和实现
在本小节中,我们将会开始设计一个支持模块化和自动服务注册的小框架,从设计和实现 Maomi.Core 开始,我们在后面的章节中会掌握更多框架技术的设计思路和实现方法,从而掌握从零开始编写一个框架的能力。
项目说明
创建一个名为 Maomi.Core 的类库项目,这个类库中将会包含框架核心抽象和实现代码。
为了减少命名空间长度,便于开发的时候引入需要的命名空间,打开 Maomi.Core.csproj 文件,在 PropertyGroup 属性中,添加一行配置:
<RootNamespace>Maomi</RootNamespace>
复制代码
配置 <RootNamespace>
属性之后,我们在 Maomi.Core 项目中创建的类型,其命名空间都会以 Maomi.
开头,而不是 Maomi.Core
。
接着为项目添加两个依赖包,以便实现自动依赖注入和初始化模块时提供配置。
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Configuration.Abstractions
复制代码
模块化设计
当本章的代码编写完毕之后,我们可以这样实现一个模块、初始化模块、引入依赖模块。代码示例如下:
[InjectModule<ApplicationModule>]
public class ApiModule : IModule
{
private readonly IConfiguration _configuration;
public ApiModule(IConfiguration configuration)
{
_configuration = configuration;
}
public void ConfigureServices(ServiceContext context)
{
var configuration = context.Configuration;
context.Services.AddCors();
}
}
复制代码
从这段代码,笔者以从上到下的顺序来解读我们需要实现哪些技术点。
1,模块依赖。
[InjectModule<ApplicationModule>]
表示当前模块需要依赖哪些模块。如果需要依赖多个模块,可以使用多个特性,示例如下:
[InjectModule<DomainModule>]
[InjectModule<ApplicationModule>]
复制代码
2,模块接口和初始化。
每一个模块都需要实现 IModule 接口,框架识别到类型继承了这个接口后才会把类型当作一个模块类进行处理。IModule 接口很简单,只有 ConfigureServices(ServiceContext context)
一个方法,可以在这个方法中编写初始化模块的代码。ConfigureServices 方法中有一个 ServiceContext 类型的参数, ServiceContext 中包含了 IServiceCollection、IConfiguration ,模块可以从 ServiceContext 中获得当前容器的服务、启动时的配置等。
3,依赖注入
每个模块的构造函数都可以使用依赖注入,可以在模块类中注入需要的服务,开发者可以在模块初始化时,通过这些服务初始化模块。
基于以上三点,我们可以先抽象出特性类、接口等,由于这些类型不包含具体的逻辑,因此从这一部分先下手,实现起来会更简单,可以避免大脑混乱,编写框架时不知道要从哪里先下手。
创建一个 ServiceContext
类,用于在模块间传递服务上下文信息,其代码如下:
public class ServiceContext
{
private readonly IServiceCollection _serviceCollection;
private readonly IConfiguration _configuration;
internal ServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
{
_serviceCollection = serviceCollection;
_configuration = configuration;
}
public IServiceCollection Services => _serviceCollection;
public IConfiguration Configuration => _configuration;
}
复制代码
根据实际需求,还可以在 ServiceContext 中添加日志等属性字段。
创建 IModule 接口。
public interface IModule
{
void ConfigureServices(ServiceContext services);
}
复制代码
创建 InjectModuleAttribute
特性,用于引入依赖模块。
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class InjectModuleAttribute : Attribute
{
// 依赖的模块
public Type ModuleType { get; private init; }
public InjectModuleAttribute(Type type)
{
ModuleType = type;
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class InjectModuleAttribute<TModule> : InjectModuleAttribute
where TModule : IModule
{
public InjectModuleAttribute() : base(typeof(TModule)){}
}
复制代码
泛型特性属于 C# 11 的新语法。
定义两个特性类后,我们可以使用 [InjectModule(typeof(AppModule))]
或 InjectModule<AppModule>
的方式定义依赖模块。
自动服务注册的设计
当完成本章的代码编写后,如果需要注入服务,只需要标记 [InjectOn]
特性即可。
// 简单注册
[InjectOn]
public class MyService : IMyService
// 注注册并设置生命周期为 scope
[InjectOn(ServiceLifetime.Scoped)]
public class MyService : IMyService
// 只注册接口,不注册父类
[InjectOn(InjectScheme.OnlyInterfaces)]
public class MyService : ParentService, IMyService
复制代码
有时我们会有各种各样的需求,例如 MyService
继承了父类 ParentService
和接口 IMyService
,但是只需要注册 ParentService
,而不需要注册接口;又或者只需要注册 MyService,而不需要注册 ParentService
、 IMyService
。
创建 InjectScheme 枚举,定义注册模式:
public enum InjectScheme
{
// 注入父类、接口
Any,
// 手动选择要注入的服务
Some,
// 只注入父类
OnlyBaseClass,
// 只注入实现的接口
OnlyInterfaces,
// 此服务不会被注入到容器中
None
}
复制代码
定义服务注册特性:
// 依赖注入标记
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class InjectOnAttribute : Attribute
{
// 要注入的服务
public Type[]? ServicesType { get; set; }
// 生命周期
public ServiceLifetime Lifetime { get; set; }
// 注入模式
public InjectScheme Scheme { get; set; }
// 是否注入自己
public bool Own { get; set; } = false;
public InjectOnAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient,
InjectScheme scheme = InjectScheme.OnlyInterfaces)
{
Lifetime = lifetime;
Scheme = scheme;
}
}
复制代码
模块依赖
因为模块之间会有依赖关系,因此为了生成模块树,需要定义一个 ModuleNode 类表示模块节点,一个 ModuleNode 实例标识一个依赖关系。
/// <summary>
/// 模块节点
/// </summary>
internal class ModuleNode
{
// 当前模块类型
public Type ModuleType { get; set; } = null!;
// 链表,指向父模块节点,用于循环引用检测
public ModuleNode? ParentModule { get; set; }
// 依赖的其它模块
public HashSet<ModuleNode>? Childs { get; set; }
// 通过链表检测是否出现了循环依赖
public bool ContainsTree(ModuleNode childModule)
{
if (childModule.ModuleType == ModuleType) return true;
if (this.ParentModule == null) return false;
// 如果当前模块找不到记录,则向上查找
return this.ParentModule.ContainsTree(childModule);
}
public override int GetHashCode()
{
return ModuleType.GetHashCode();
}
public override bool Equals(object? obj)
{
if (obj == null) return false;
if(obj is ModuleNode module)
{
return GetHashCode() == module.GetHashCode();
}
return false;
}
}
复制代码
框架在扫描所有程序集之后,通过 ModuleNode 实例将所有模块以及模块依赖组成一颗模块树,通过模块树来判断是否出现了循环依赖。
比如,有以下模块和依赖:
[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。
C.Childs = new (){ A , B}
A.ParentModule => C
B.ParentModule => C
复制代码
当把 A、B、C 三个模块以及依赖关系扫描完毕之后,会得到以下的模块依赖树。一个节点即是一个 ModuleNode 实例,一个模块被多次引入,就会出现多次。
那么,如果识别到循环依赖呢?只需要调用 ModuleNode.ContainsTree()
从一个 ModuleNode 实例中,不断往上查找 ModuleNode.ParentModule
即可,如果该链表中包含相同类型的模块,即为循环依赖,需要抛出异常。
比如从 C0 开始,没有父节点,则不存在循环依赖。
从 A0 开始,A0 -> C0 ,该链路中也没有出现重复的 A 模块。
从 C1 开始,C1 -> A0 -> C0 ,该链路中 C 模块重复出现,则说明出现了循环依赖。
所以,是否出现了循环依赖判断起来是很简单的,我们只需要从 ModuleNode.ContainsTree()
往上查找即可。
在生成模块树之后,通过对模块树进行后序遍历即可。
比如,有以下模块以及依赖。
[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>();
复制代码
伪代码示例如下:
private static void InitModuleTree(ModuleNode moduleNode)
{
if (moduleNode.Childs != null)
{
foreach (var item in moduleNode.Childs)
{
InitModuleTree(item);
}
}
// 如果该节点已经没有子节点
// 如果模块没有处理过
if (!moduleTypes.Contains(moduleNode.ModuleType))
{
InitInjectService(moduleNode.ModuleType);
}
}
复制代码
未完待续......
文章转载自:痴者工良
原文链接:https://www.cnblogs.com/whuanle/p/18227954
体验地址:http://www.jnpfsoft.com/?from=infoq
评论