ThunderboltIoc:.NET 无反射依赖注入!






4.90/5 (8投票s)
介绍和文档,适用于不使用反射在 .NET 中实现 DI 的新 ThunderboltIoc 框架
引言
最早的 .NET IoC 框架之一,不使用反射。一个在闪电落下之前就投射其服务的 IoC。 😄
目标是为 .NET 创建一个基于代码生成的 IoC 容器。如今,几乎所有的容器都依赖反射来提供功能的 IoC,这是一项成本高昂的操作。尽管多年来人们为提高这些容器的性能付出了宝贵的努力,但如果能够实现基于代码生成的 IoC,这些容器的性能将无法与之匹敌。令人恼火的是,我们为了通过——除其他方法外——使用基于反射的 IoC 框架来清理我们的代码而付出的代价,却对此无能为力。想法是让用户(开发人员)随意编写他们的代码,并让 IoC 在编译期间负责代码生成,从而在目标程序集中提供一个有效且高性能的解决方案。
我终于抽出时间开始着手解决了。幸运的是,在开始之前,我阅读了有关 .NET 中源生成器的信息。目睹了源生成器与 Roslyn 的强大结合,我决定选择这条路径,而不是 T4 模板和 CodeDom 等代码生成方法,我很高兴我做出了这个选择。我的解决方案可能不是第一个采用这种方法,但我喜欢认为它是迄今为止最强大、最灵活的解决方案。
下面是关于该框架的文档、功能概述和快速入门指南。您可以在GitHub 存储库上找到完整的源代码。
我也欢迎您的建议。请随时在下方留言联系我。
附注:原文中的“如果能够实现”这句话出现在我 2021 年 3 月的原始初稿中。尽管我今天已经设法实现了这一点,但我还是把它留在这里,以进一步激励读者。
目录
- 安装
- 快速入门
- 功能概述
- 基准测试
- 服务生命周期
- 服务注册
- 如何解析/获取您的实例
- 支持的项目类型
- 已知限制
- 计划在未来版本中实现
- 终于!
- 历史
1. 安装
ThunderboltIoc 的安装与将 nuget 安装到目标程序集一样简单。无需进一步配置。但是,为了注册您的服务,您需要在每个可能需要注册服务的项目中实现(即创建一个继承自)ThunderboltRegistration
的**部分类**。您可以在 Nuget.org 上找到该包:使用 dotnet
dotnet add package ThunderboltIoc
或使用 Nuget 包管理器控制台
Install-Package ThunderboltIoc
1.1. 确保您使用的是 C# 9.0 或更高版本
在每个引用了 ThunderboltIoc 的项目中,确保使用的 C# 版本是 9.0
或更高版本。在您的 *.csproj 文件中添加
<PropertyGroup>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
请注意,C# 9.0 仅在 Visual Studio 2022 中受支持,而 Visual Studio 2019 从版本 16.7
开始也支持。
1.2. 将 ThunderboltRegistration 实现为**部分**类
如果您不希望在此程序集中注册任何类型/服务,则不需要此步骤。但是,如果您有(这很可能),您应该创建一个继承自 ThunderboltRegistration
**抽象**类的**部分**类。有关注册的更多信息,请参阅本文档的相关部分。
2. 快速入门
安装后,这是一个最小化的工作示例
2.1. 在您的 Assembly FooAssembly 的任何位置
public partial class FooThunderboltRegistration : ThunderboltRegistration
{
protected override void Register(IThunderboltRegistrar reg)
{
reg.AddSingleton<BazService>();
reg.AddScoped<IBarService, BarService>();
reg.AddTransientFactory<Qux>(() => new Qux());
}
}
2.2. 在您的启动代码中
在任何尝试解析/获取您的服务之前,此代码可能会在此执行。
ThunderboltActivator.Attach<FooThunderboltRegistration>();
2.3. 使用您的服务
获取服务的最简单方法如下
BazService bazService = ThunderboltActivator.Container.Get<BazService>();
3. 功能概述
- 在 .NET 中实现依赖注入,无需反射,基于 Roslyn 源生成器,具有简单直观的 API
- 能够以三种不同的生命周期注册您的服务:单例 (Singleton)、作用域 (Scoped) 和瞬态 (Transient)
- 显式注册,指示框架注册特定服务
- 能够注册服务,同时指定用户定义的工厂来创建它们,同时保持其生命周期
- 能够注册服务,同时指定用户定义的逻辑来确定服务实现的类型
- 基于属性的服务注册,您可以使用属性来注册或排除类型
- 通过约定注册,我们可以使用正则表达式按命名约定注册服务
4. 基准测试
src/benchmarks
项目使用 BenchmarkDotNet 对 ThunderboltIoc 与以下依赖注入框架进行衡量性能比较
-
Microsoft.Extensions.DependencyInjection(在nuget.org上)- 基准测试图例:MicrosoftDI .NET 的行业标准依赖注入框架,由 Microsoft 提供,具有基本功能和高性能。它依赖于运行时表达式编译来创建服务。
-
Grace(在nuget.org上)- 以其广泛的功能和卓越的性能而闻名。它依赖于
System.Reflection.Emit
运行时代码生成来创建服务。值得一提的是,iOS 不支持运行时代码生成,因此 Grace 在 Xamarin 应用或客户端应用方面选项有限。
-
Autofac(在nuget.org上)- 以其丰富的功能而闻名。它使用原始反射来实例化服务,但这会付出高昂的代价,正如基准测试所示。
尽管 ThunderboltIoc 很新,但它试图结合这些框架中最好的部分,避免最坏的部分,并优化内存使用。基准测试使用不同的运行策略进行多次运行,并衡量每个框架在启动和运行时性能,其中启动是指完全创建和配置容器所需的时间,运行时是指获取/解析/定位少量服务所需的时间。
我将首先分享我机器上的一个基准测试运行示例,然后分享一些关于这些数字含义的见解。
以上数据按均值列(从最快到最慢)排序。然而,均值可能会(并且确实)受到少数异常值的影响,这就是为什么我包含了基线比率和中位数列。其中一些中位数提供了证据,表明它们对应的均值受到异常值的影响。
然而,为了更准确地了解哪个框架在哪个场景下表现更好,我们应该查看比率列。ThunderboltIoc 将始终有一个比率,在每个场景中,其他框架将具有成比例的值,其中较小的值表示在该场景下性能更好,较大的值表示性能较差。
对于不熟悉 BenchmarksDotNet 的人来说,比率常常与相互成比例的最终均值混淆。这并非如此。相反,在每次运行中,比率都会被计算并存储在该特定操作中,最后,显示的比率将是所有计算出的比率的均值。这提供了对异常值更好的免疫力。
还值得一提的是,ThunderboltIoc 的分配内存量在每个场景中都占主导地位(或等于最小值)。
5. 服务生命周期
Thunderbolt 的服务生命周期非常相似(实际上,几乎相同)于 Microsoft.Extensions.DependencyInjection
,后者最初是在 .NET Core 中引入的。
5.1. 单例 (Singleton)
指定在程序的整个生命周期中只会创建一个服务实例。
5.2. 作用域 (Scoped)
指定为每个作用域都会创建一个新的服务实例。如果在解析服务时没有可用的作用域(即使用的 IThundernoltResolver
不是 IThunderboltScope
),则会返回一个单例服务。
如果使用 IThunderboltScope
作为 IThunderboltResolver
解析服务,那么该服务的所有作用域依赖项都将使用相同的 IThunderboltScope
进行解析(除非它是一个单例服务,其作用域依赖项已在之前解析)。
5.3. 瞬态 (Transient)
指定每次请求时都会创建一个新的服务实例。
6. 服务注册
除了默认为您注册的服务之外,您还可以通过以下三种方式注册您的程序集:
- 显式注册
- 属性注册
- 基于正则表达式的属性注册
所有这三种方法都可以单独使用,也可以与其他方法结合使用。事实上,源生成器为属性注册生成显式注册。
6.1. 默认为您注册的服务
-
IThunderboltContainer
:容器本身被注册为单例服务。事实上,您可以使用ThunderboltActivator.Container
来获取容器,它会返回相同的单例实例。 -
IThunderboltScope
:这被注册为瞬态服务,这意味着每次尝试解析/获取IThunderboltScope
时,都会创建一个新的IThunderboltScope
。这与IThunderboltContainer.CreateScope()
相同。值得一提的是,
IThunderboltScope
是IDisposable
,但是,这并不意味着处置IThunderboltScope
会处置作用域实例(至少在当前版本中)。 -
IThunderboltResolver
:这被注册为瞬态服务,并返回用于获取此实例的IThunderboltResolver
。这只会返回IThunderboltContainer
或IThunderboltScope
。
6.2. 显式注册
在创建了一个继承自 ThunderboltRegistration
的**部分**类之后,您必须重写抽象方法 void Register
。此方法为您提供一个类型为 IThunderboltRegistrar
(reg
)的参数,您可以使用它通过 Add{serviceLifetime}
方法来注册您的服务。
对于显式注册,代码生成将发生在 Add{serviceLifetime}
调用内部的每个调用(工厂注册除外),无论您是否在 Register 方法中实现逻辑(也就是说,即使您实现了某个逻辑,使得某个 Add
方法的调用不可达,代码生成仍然会考虑它)。
显式注册支持四种不同的场景来注册您的服务。
6.2.1. 相同的服务和实现类型
在这种情况下,您可以像这样注册您的服务:
reg.AddSingleton<FooService>();
reg.AddScoped<BarService>();
reg.AddTransient<BazService>();
6.2.2. 不同的服务和实现类型
在这种情况下,您可以像这样注册您的服务:
reg.AddSingleton<IFooService, FooService>();
reg.AddScoped<IBarService, BarService>();
reg.AddTransient<IBazService, BazService>();
6.2.3. 服务有工厂来确定生成的实例
在这种情况下,您可以像这样注册您的服务:
reg.AddSingletonFactory<FooService>();
reg.AddScopedFactory<BarService>();
reg.AddTransientFactory<BazService>();
在这种情况下,根本不会发生代码生成,因为假定您将提供获取此服务实例所需的所有详细信息。但是,使用任何工厂签名注册的服务仍然会应用服务生命周期。
6.2.4. 服务有运行时逻辑来确定其实现
在这种情况下,您可以像这样注册您的服务:
reg.AddSingleton<IYearHalfService>(() =>
{
if (DateTime.UtcNow.Month <= 6)
return typeof(YearFirstHalfService);
else
return typeof(YearSecondHalfService);
});
在这种情况下,重要的是要知道,对于作用域内 return typeof(TService)
语句的每次捕获都会发生代码生成。这意味着您的实现必须在编译时可知。同样重要的是要注意,以下任一
Type fooType = typeof(FooType);
return fooType;
或
return someFooVariable.GetType();
将不起作用。
此外,您不能将函数指针传递给此签名。它会编译但无法正常工作。唯一接受的语法是 lambda 表达式(例如 () => typeof(TService)
或 () => { return typeof(TService); }
)和匿名方法表达式(例如 delegate { return typeof(TService); }
)。
6.3. 属性注册
您可以在这里使用属性注册您的服务。重要的是要知道,即使您没有显式注册,您仍然需要创建一个继承自 ThunderboltRegistration
的**部分**类才能使属性注册生效。属性注册通过两个属性(+ 正则表达式属性)进行管理:ThunderboltIncludeAttribute
和 ThunderboltExcludeAttribute
。
6.3.1. ThunderboltIncludeAttribute
在您要注册的类型的顶部定义此属性。该属性有几种签名,但它们都归结为三个简单的参数。
serviceLifetime
(必需):指定要为此服务注册的服务生命周期。implementation
(可选):如果此服务具有其他类型作为其实现,则可以通过此方式指定。applyToDerviedTypes
(可选,默认为false
):确定派生类型是否也应像此服务一样被注册。需要注意的是,捕获的派生类型仅限于在定义该属性的程序集中可访问的类型;也就是说:如果您有一个程序集FooAssembly
,其中使用了ThunderboltIncludeAttribute
(applyToDerivedTypes
为true
),并且另一个程序集BarAssembly
引用了FooAssembly
但也定义了一些派生类型,那么BarAssembly
中的类型将不会被注册(除非BarAssembly
中也定义了另一个ThunderboltIncludeAttribute
)。
6.3.2. ThunderboltExcludeAttribute
指定除非通过 ThunderboltRegistration.Register
显式注册,否则该类型将不会被注册(即使存在 ThunderboltIncludeAttribute
或 ThunderboltRegexIncludeAttribute
)。
此属性还具有可选参数(默认为 false
)applyToDerivedTypes
,它指定在此程序集中可访问的派生类型不应通过属性注册属性进行注册。
6.4. 基于正则表达式的属性(约定)注册
您可以通过其命名约定一次注册多个服务。与非正则表达式属性注册一样,即使您没有显式注册,也需要创建一个继承自 ThunderboltRegistration
的**部分**类才能使其生效。正则表达式属性注册通过两个属性进行管理:(ThunderboltRegexIncludeAttribute
和 ThunderboltRegexExcludeAttribute
)。
用于正则表达式注册的属性是程序集级别的属性(应在目标程序集 [assembly: ThunderboltRegexIncludeAttribute(...)]
上定义)。
6.4.1. ThunderboltRegexIncludeAttribute
在这里定义您的命名约定注册约定。传递给此属性的第一个参数是匹配此属性的所有服务的服务生命周期。
传递给属性的正则表达式参数应匹配您希望注册的所有类型。您的模式将根据类型的完整名称(global::Type.Full.Namespace.TypeName
)进行测试。
请注意,您的模式将针对所有可访问的类型进行测试,这意味着您可能希望编写一个不包含 System 命名空间下定义的类型的模式。
您可以在同一个程序集中拥有多个 ThunderboltRegexIncludeAttribute
来定义不同的模式和生命周期。
6.4.1.1. 注册本身就是其实现的服务
仅使用上述两个参数足以满足此场景。
6.4.1.2. 注册具有不同实现类型的服务
在这种情况下,除了生命周期和正则表达式之外,还需要另外两个参数。
implRegex
:这应该是一个正则表达式,用于匹配您所有的实现类型(且仅匹配您的实现类型)。joinKeyRegex
:到目前为止,您的正则表达式参数应该匹配许多服务,您的implRegex
参数应该匹配许多服务实现。此参数用于从两个其他参数匹配的结果中选择一个特定的连接键,以确定哪些实现对应于哪些服务。这同样是一个正则表达式。
6.4.1.2.1 IFooService: FooService:对于我们可能希望将 IFooService 与 FooService 匹配的常见场景,我们可以使用以下方法:
[assembly: ThunderboltRegexInclude(
ThunderboltServiceLifetime.Scoped,
regex: @"(global::)?(MyBaseNamespace)\.I[A-Z][A-z_]+Service",
implRegex: @"(global::)?(MyBaseNamespace)\.[A-Z][a-z][A-z_]+Service",
joinKeyRegex: @"(?<=(global::)?(MyBaseNamespace)\.I?)[A-Z][a-z][A-z_]+Service")]
6.4.2. ThunderboltRegexExcludeAttribute
在这里,您可以指定一个正则表达式,当它与任何类型匹配时,该类型将不会通过属性注册(和/或基于正则表达式的属性注册)进行注册。
7. 如何解析/获取您的实例
获取服务实例(根据其注册的生命周期)就像使用 Get<TService>()
在 IThunderboltResolver
上一样简单。IThunderboltResolver
可以是 IThunderboltContainer
或 IThunderboltScope
。
7.1. 获取 IThunderboltContainer
假定您已经通过 ThunderboltActivator.Attach
附加了至少一个继承自 ThunderboltRegistration
的**部分**类,那么使用 ThunderboltActivator.Container
属性获取单例 IThunderboltContainer
实例是安全的。如果您已经有了 IThunderboltResolver
,您也可以使用 resolver.Get<IThunderboltContainer>()
来获取同一个容器。
7.2. 获取 IThunderboltScope
使用 IThunderboltContainer
,您可以创建一个新的作用域,使用 container.CreateScope()
。如果您已经有了 IThunderboltResolver
,您可以使用 resolver.Get<IThunderboltScope>()
来创建一个新的作用域。
8. 支持的项目类型
所有项目类型都自然支持,您的项目在使用 ThunderboltIoc
时不会出现问题。然而,在撰写本文时,尚未与 Microsoft.Extensions.DependencyInjection
进行显式集成,这在一定程度上限制了我们在使用 .NET Core 项目时利用 Microsoft DI 的选择。
未来计划将 ThunderboltIoc
与 Microsoft.Extensions.DependencyInjection
集成,但在此期间,将这两个框架并排使用也没有坏处;然而,这并不能让我们一直充分利用 ThunderboltIoc
的卓越性能。
将 ThunderboltIoc
用作独立的 IoC 框架,用于任何 .NET C# 项目都是完全安全的。
9. 已知限制
唯一支持代码生成的服务是具有唯一公共构造函数的服务。计划在未来版本中解除此限制。
10. 计划在未来版本中实现
10.1. 移除单个公共构造函数限制
如上一节所述,消除此限制是可行且期望的。
10.2. 更好的源生成异常处理
目前,在最佳情况下,如果您遵循文档,我们不应担心源生成异常。然而,如果未能遵守文档,可能会出现未处理的异常。在这种情况下,源生成器可能会(也可能不会)完全失败。当发生这种情况时,可能根本不会生成任何代码(您会在构建输出中收到通知,但可能不会注意到)。
计划提供更好的异常处理,这样为特定服务生成代码的失败就不会导致整个过程失败。此外,生成相关的警告或错误也会很有用。
10.3. 验证是否存在循环依赖
目前,可能会陷入无限解析操作,其中一个(或多个)服务的依赖项可能直接或间接依赖于同一服务。
10.4. 分析器
静态代码分析器能够向您显示关于未能遵循文档中讨论的最佳实践的警告,这将是有益的。例如,生成警告,告诉您创建的继承自 ThunderboltRegistration
的类没有 partial 修饰符。
10.5. 属性注入
与任何功能丰富的 IoC 框架一样,属性注入是一种依赖注入样式,目前此框架不支持。
10.6. 自动对象处置
处置 IThunderboltScope
应该相应地处置保存在相应 IThunderboltScope
中的每个 IDisposable
作用域服务。
10.7. 与 Microsoft.Extensions.DependencyInjection 集成
目标是提供一个直观的 API 来有效替换 Microsoft DI 的默认 IServiceProvider
。目前,IThunderboltResolver
已经实现了 System.IServiceProvider
,但目前还没有优雅的集成。
终于!
希望这个版本能达到您的期望!我非常期待在下面的评论中听到您的反馈和建议。
我的目的是在新的版本发布时更新这篇文章。如果您有兴趣,可以关注一下。
历史
- 2022 年 1 月 18 日:初始版本