SteroidsDI 1.4.2
dotnet add package SteroidsDI --version 1.4.2
NuGet\Install-Package SteroidsDI -Version 1.4.2
<PackageReference Include="SteroidsDI" Version="1.4.2" />
paket add SteroidsDI --version 1.4.2
#r "nuget: SteroidsDI, 1.4.2"
// Install SteroidsDI as a Cake Addin #addin nuget:?package=SteroidsDI&version=1.4.2 // Install SteroidsDI as a Cake Tool #tool nuget:?package=SteroidsDI&version=1.4.2
SteroidsDI
<a href="https://www.buymeacoffee.com/sungam3r" target="_blank"><img src="https://bmc-cdn.nyc3.digitaloceanspaces.com/BMC-button-images/custom_images/orange_img.png" alt="买我一杯咖啡" style="height: auto !important;width: auto !important;" ></a>
每天使用的先进依赖注入。
安装
此存储库提供以下包
您可以通过 NuGet 安装最新稳定版本。
> dotnet add package SteroidsDI
> dotnet add package SteroidsDI.Core
> dotnet add package SteroidsDI.AspNetCore
它是做什么的?我为什么需要它?
.NET Core 原生支持 依赖注入。它运行良好。我们可以使用三个主要生命周期的依赖项:单例、作用域、瞬态。存在规则指定了具有不同生命周期的对象中传递对象的可能组合。例如,您可能会遇到以下错误消息
验证服务描述符 '服务类型:I_XXX 生命周期:单例 实现类型:XXX' 时出错:无法从单例 'I_XXX' 消费作用域服务 'YYY'。
错误表明您不能将具有较短期限的对象传递给长期生活的对象的构造函数。这是正确的!问题很清楚。但如何解决它?显然,当使用 构造函数依赖注入(.NET Core 原生支持仅限于构造函数 DI)时,我们必须遵循这些规则。所以至少有以下 3 个选项
- 延长注入对象的存活期。
- 缩短注入依赖对象的存活期。
- 移除此类依赖。
- 更改依赖的设计以满足规则。
第一种方法远非总是可能。第二种方法更容易实现,尽管这会因为重复创建之前已创建的对象而降低性能。第三种方式...好吧,你懂的,生活不会变得容易。因此,我们需要改变设计。本项目提供了一种解决问题的方法,引入了许多辅助抽象。正如许多人所知道
任何编程问题都可以通过引入一个附加的抽象层来解决,除了过多的抽象的难题。
本项目提供了三种这样的抽象
- 众所周知
Func<T>
委托。 Defer<T>
/IDefer<T>
抽象,它们看起来像Lazy<T>
,但有一个重大的区别 -Defer<T>
/IDefer<T>
不会缓存值。- 当运行时生成实现类型时的命名工厂接口。
所有这些抽象 都解决了相同的问题,从不同角度接近它们的API设计。挑战是通过某个中介对象X提供依赖T,其中对T的显式依赖要么不可能,要么不希望存在。重要!本包中没有任何实现缓存依赖T。
如上所述,不可能性的例子是对具有单例生命周期的对象中具有作用域生命周期的依赖。不希望存在性的例子是创建依赖很昂贵,并不总是需要。
需要指出的一点是 - 注入依赖与使用依赖不同。事实上,在构造函数注入的情况下,构造函数中的依赖注入(在大多数情况下)导致将传递值的引用存储在某些字段中。依赖将在稍后调用“父”对象的方法时使用。
Func<T>
此方法是最简单的,提供注入 Func<T>
而不是 T
之前
class MyObject
{
private IRepository _repo;
public MyObject(IRepository repo) { _repo = repo; }
public void DoSomething() { _repo.DoMagic(); }
}
之后
class MyObject
{
private Func<IRepository> _repo;
public MyObject(Func<IRepository> repo) { _repo = repo; }
public void DoSomething() { _repo().DoMagic(); }
}
在DI中如何配置
public void ConfigureServices(IServiceCollection services)
{
// First register your IRepository and then call
services.AddFunc<IRepository>();
}
请注意,您应该对每个想要作为 Func<T>
注入的依赖 T
调用 AddFunc
。
IDefer<T> 和 Defer<T>
这种方法建议更明确的API - 注入 IDefer<T>
或 Defer<T>
而不是 T
之前
class MyObject
{
private IRepository _repo;
public MyObject(IRepository repo) { _repo = repo; }
public void DoSomething() { _repo.DoMagic(); }
}
之后
class MyObject
{
private Defer<IRepository> _repo;
public MyObject(Defer<IRepository> repo) { _repo = repo; }
public void DoSomething() { _repo.Value.DoMagic(); }
}
在DI中如何配置
public void ConfigureServices(IServiceCollection services)
{
// First register your IRepository and then call
services.AddDefer();
}
请注意,与 AddFunc<T>
不同,AddDefer
方法只需要调用一次。如果需要协变,请使用 IDefer<T>
接口。
命名工厂
此方法最难实现,但从公共API的角度来看,它和前两种方法一样简单。它假设你声明了一个带有一个或多个无参方法的工厂接口。方法名无关紧要。每个工厂方法应返回DI容器中配置的某些依赖类型。
public interface IRepositoryFactory
{
IRepository GetPersonsRepo();
}
并将这个工厂注入到你的“父”类型中
class MyObject
{
private IRepositoryFactory _factory;
public MyObject(IRepositoryFactory factory) { _factory = factory; }
public void DoSomething() { _factory.GetPersonsRepo().DoMagic(); }
}
在DI中如何配置
public void ConfigureServices(IServiceCollection services)
{
// First register your IRepository and then call
services.AddFactory<IRepositoryFactory>();
}
IRepositoryFactory
的实现将在运行时生成。
实际上,每个工厂方法都可以接受任意类型的一个参数 - 字符串、枚举、自定义类等。在这种情况下,应指定一个 命名绑定。然后你可以在工厂方法中通过传递绑定的名称来解析所需的服务。如果你想要提供默认实现,则可以配置 默认绑定。默认绑定是在没有命名绑定时使用的绑定。用户应明确设置默认绑定,以便能够解析未注册的名称的服务。
public interface IRepositoryFactory
{
IRepository GetPersonsRepo(string mode);
}
public interface IRepository
{
void Save(Person person);
}
public class DemoRepository : IRepository
{
...
}
public class ProductionRepository : IRepository
{
...
}
public class RandomRepository : IRepository
{
...
}
public class DefaultRepository : IRepository
{
...
}
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IRepository, DemoRepository>()
.AddTransient<IRepository, ProductionRepository>()
.AddTransient<IRepository, RandomRepository>()
.AddFactory<IRepositoryFactory>()
.For<IRepository>()
.Named<DemoRepository>("demo")
.Named<ProductionRepository>("prod")
.Named<RandomRepository>("rnd")
.Default<DefaultRepository>();
}
public class Person
{
public string Name { get; set; }
}
public class SomeClassWithDependency
{
private readonly IRepositoryFactory _factory;
public SomeClassWithDependency(IRepositoryFactory factory)
{
_factory = factory;
}
private bool SomeInterestingCondition => ...
public void DoSomething(Person person)
{
if (person.Name == "demoUser")
_factory.GetPersonsRepo("demo").Save(person); // DemoRepository
else if (person.Name.StartsWith("tester"))
_factory.GetPersonsRepo("rnd").Save(person); // RandomRepository
else if (SomeInterestingCondition)
_factory.GetPersonsRepo("prod").Save(person); // ProductionRepository
else
_factory.GetPersonsRepo(person.Name).Save(person); // DefaultRepository
}
}
在上面的例子中,GetPersonsRepo
方法将返回为提供的名称配置的相应的 IRepository
接口的实现。对于所有未注册的名称(包括空值),它将返回 DefaultRepository
。
它是如何工作的?
这里的一切都很简单。三种方法都归结为将依赖项解析委派给恰当的 IServiceProvider。这里的“恰当”是什么意思?通常情况下,在 ASP.NET Core 应用程序中,每个人都习惯于使用一个(作用域)从 IHttpContextAccessor
获取的提供者 —— HttpContext.RequestServices
。但在一般情况下,可能有多个这样的提供者。此外,消耗依赖项的代码并不知道它们的存在。这段代码可能是一个通用库,与应用程序特定的环境没有紧密耦合。因此,引入了对获取恰当的 IServiceProvider 的抽象。是的,又是一种抽象!
本项目提供了两个内置提供程序
AspNetCoreHttpScopeProvider
,用于 ASP.NET Core 应用程序。GenericScopeProvider{T}
,用于通用库。
在DI中如何配置
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpScope();
services.AddGenericScope<SomeClass>();
}
当然,你也可以始终编写自己的提供程序
public class MyScopeProvider : IScopeProvider
{
public IServiceProvider? GetScopedServiceProvider(IServiceProvider rootProvider) => rootProvider.ReturnSomeMagic();
}
并为其在 DI 中提供注册
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMyScope(this IServiceCollection services)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton<IScopeProvider, MyScopeProvider>());
return services;
}
}
高级行为
你可以通过 ServiceProviderAdvancedOptions 来自定义 AddFunc
/AddDefer
/AddFactory
APIs 的行为:只需使用来自 Microsoft.Extensions.Options/Microsoft.Extensions.Options.ConfigurationExtensions
包的标准扩展方法。
public void ConfigureServices(IServiceCollection services)
{
services.Configure<ServiceProviderAdvancedOptions>(options => options.AllowRootProviderResolve = true)
services.Configure<ServiceProviderAdvancedOptions>(Configuration.GetSection("Steroids"));
}
示例
您可以在 示例项目 中看到如何使用上述所有 API。
常见问题解答
问题。等等。难道 Microsoft.Extensions.DependencyInjection
没有提供开箱即用的支持吗?
答案。很遗憾没有。我宁愿使用现有的功能,也不愿自己编写软件包。
<br/>
问题。您提供的是否是一种 ServiceLocator?我听说 ServiceLocator 是一种反模式。
答案。是的,ServiceLocator 是一种已知的反模式。这里可以找到关于 ServiceLocator 的描述。与所提出解决方案的本质区别在于,ServiceLocator 允许您在运行时解决任何依赖项,而 Func<T>
、Defer<T>
和命名工厂仅设计用于解决在编译时指定的已知依赖项。因此,主要区别是类的所有依赖项都显式声明并注入其中。该类本身 不会 在其实现中秘密地获取这些依赖项。这就是所谓的 显式依赖原理。
<br/>
问题。这是一种新的依赖注入方法吗?
答案。实际上不是。这种方法的描述可以在多年前的文章/博客中找到,例如 这里。
<br/>
问题。我应该选择 Func<>
还是 [I]Defer<>
?
答案。最重要的是,它们在底层都工作得同样好。区别在于您打算在什么环境中使用这些 API。
主要有两个区别
Func<>
的优势在于,注入Func<>
的代码不需要任何新的依赖项,它是一个众所周知 的 .NET 委托类型。相反,[I]Defer<>
需要引用SteroidsDI.Core
包。对于每个您希望以
Func<T>
形式注入的依赖项T
,您应该调用AddFunc
。相反,只需要调用一次AddDefer
方法。
<br/>
问题。如果我想创建自己的作用域来使用,也就是说,不仅要消耗它,还要提供它怎么办?
A. 首先,您需要以某种方式获取根 IServiceProvider
的实例。然后通过调用其在 CreateScope()
方法上创建作用域并将它设置为 GenericScope
var rootProvider = ...;
using var scope = rootProvider.CreateScope();
GenericScope<SomeClass>.CurrentScope = scope;
...
... Some code here that works with scopes.
... All registered Func<T>, [I]Defer<T> and
... factories use created scope.
...
GenericScope<T>.CurrentScope = null;
或者,您可以使用更简单的 Scoped<T>
结构。
IScopeFactory scopeFactory = ...; // can be obtained from DI, see AddMicrosoftScopeFactory extension method
using (new Scoped<SomeClass>(scopeFactory))
or
await using (new Scoped<SomeClass>(scopeFactory)) // Scoped class supports IAsyncDisposable as well
{
...
... Some code here that works with scopes.
... All registered Func<T>, [I]Defer<T> and
... factories use created scope.
...
}
有关更多信息,请参见 ScopedTestBase 和 ScopedTestDerived。这个示例展示了如何将作用域支持添加到所有单元测试中。
基准测试
结果可在 此处查看。
产品 | 版本 兼容的和额外的计算目标框架版本。 |
---|---|
.NET | net5.0 已计算。 net5.0-windows 已计算。 net6.0 已计算。 net6.0-android 已计算。 net6.0-ios 已计算。 net6.0-maccatalyst 已计算。 net6.0-macos 已计算。 net6.0-tvos 已计算。 net6.0-windows 已计算。 net7.0 已计算。 net7.0-android 已计算。 net7.0-ios 已计算。 net7.0-maccatalyst 已计算。 net7.0-macos 已计算。 net7.0-tvos 已计算。 net7.0-windows 已计算。 net8.0 已计算。 net8.0-android 已计算。 net8.0-browser 已计算。 net8.0-ios 已计算。 net8.0-maccatalyst 已计算。 net8.0-macos 已计算。 net8.0-tvos 已计算。 net8.0-window 已计算。 |
.NET Core | netcoreapp2.0 已计算。 netcoreapp2.1 已计算。 netcoreapp2.2 已计算。 netcoreapp3.0 已计算。 netcoreapp3.1 已计算。 |
.NET Standard | netstandard2.0 兼容。 netstandard2.1 已计算。 |
.NET Framework | net461 已计算。 net462 已计算。 net463 已计算。 net47 已计算。 net471 已计算。 net472 已计算。 net48 已计算。 net481 已计算。 |
MonoAndroid | monoandroid 已计算。 |
MonoMac | monomac 已计算。 |
MonoTouch | monotouch 已计算。 |
Tizen | tizen40 已计算。 tizen60 已计算。 |
Xamarin.iOS | xamarinios 已计算。 |
Xamarin.Mac | xamarinmac 已计算。 |
Xamarin.TVOS | xamarintvos 已计算。 |
Xamarin.WatchOS | xamarinwatchos 已计算。 |
-
.NETStandard 2.0
- Microsoft.Extensions.Options (>= 1.0.0)
- SteroidsDI.Core (>= 1.4.2)
- System.Reflection.Emit (>= 4.7.0)
NuGet 包
此包未由任何 NuGet 包使用。
GitHub 仓库
此包未由任何热门 GitHub 仓库使用。
版本 | 下载 | 最后更新 |
---|---|---|
1.4.2 | 2,376 | 3/18/2024 |
1.4.1 | 1,935 | 1/20/2024 |
1.4.0 | 437 | 1/4/2024 |
1.3.0 | 743 | 11/18/2023 |
1.2.0 | 9,987 | 12/29/2022 |
1.1.1 | 2,332 | 12/1/2022 |
1.1.0 | 6,752 | 5/2/2022 |
1.0.3 | 889 | 4/15/2022 |
1.0.2 | 1,325 | 3/31/2022 |
1.0.1 | 7,833 | 10/16/2021 |
1.0.0 | 16,171 | 2/26/2021 |
0.0.7 | 4,214 | 11/24/2020 |
0.0.6 | 706 | 11/24/2020 |
0.0.5 | 1,731 | 10/26/2020 |
0.0.4 | 18,868 | 6/25/2020 |
0.0.3 | 950 | 3/10/2020 |
0.0.2 | 770 | 3/9/2020 |
0.0.1 | 850 | 3/8/2020 |