SteroidsDI.AspNetCore 1.4.2
dotnet add package SteroidsDI.AspNetCore --version 1.4.2
NuGet\Install-Package SteroidsDI.AspNetCore -Version 1.4.2
<PackageReference Include="SteroidsDI.AspNetCore" Version="1.4.2" />
paket add SteroidsDI.AspNetCore --version 1.4.2
#r "nuget: SteroidsDI.AspNetCore, 1.4.2"
// Install SteroidsDI.AspNetCore as a Cake Addin #addin nuget:?package=SteroidsDI.AspNetCore&version=1.4.2 // Install SteroidsDI.AspNetCore as a Cake Tool #tool nuget:?package=SteroidsDI.AspNetCore&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="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>
日常使用的先进依赖注入。
安装
此存储库提供以下包
您可以通过 NuGet 安装最新稳定版本。
> dotnet add package SteroidsDI
> dotnet add package SteroidsDI.Core
> dotnet add package SteroidsDI.AspNetCore
它是什么?为什么我需要它?
C# .NET Core 内置了对 依赖注入(Dependency Injection) 的支持。它效果非常好。我们可以使用三个主要生命周期的依赖关系:单例(Singleton)、作用域(Scoped)、瞬态(Transient)。存在一些规则,用于指定传递具有一种生命周期的对象到具有另一种生命周期的对象的可能组合。例如,你可能会遇到这样的错误消息
在验证服务描述符时出现错误:'ServiceType: I_XXX Lifetime: Singleton ImplementationType: XXX':无法从单例 'I_XXX' 中消耗作用域服务 'YYY'。
错误说明不能将具有较短生命周期的对象传递给具有较长生命周期的对象的构造函数。这是正确的!问题很明显。但如何解决它呢?很明显,当使用 构造函数依赖注入(.NET Core 只支持构造函数依赖注入)时,我们必须遵循这些规则。因此,至少有 3 种选择
- 延长注入对象的生存期。
- 缩短提供依赖项的对象的生存期。
- 删除这样的依赖项。
- 更改依赖项的设计,以满足规则。
第一种方法并不总是可行。第二种方法更容易实现,尽管这会导致性能下降,因为重复创建了以前只需要创建一次的对象。第三种方法... 理解,生活不会变得更容易。所以我们需要在某种程度上改变设计。这个项目就提供了这样的解决问题的方式,引入了几个辅助抽象。正如许多人已经知道的
可以通过引入额外的抽象层次来解决问题,但是并不总是解决过多抽象的问题。
该项目提供了三种这样的抽象
- 众所周知
Func
委托。 Defer
/IDefer
抽象,它们看起来像Lazy
,但有一个显著的区别 -Defer
/IDefer
不缓存值。- 一个命名工厂接口,在运行时生成实现类型。
所有这些抽象都 解决了相同的问题,从不同角度来设计它们的 API。挑战是通过某些中介对象 X 将依赖项 T 注入,其中对 T 的显式依赖要么不可能,要么不希望拥有。 重要!本软件包中没有任何实现缓存依赖项 T。
如上所述,一个不可能的例子是对具有单例生命周期的对象的具有作用域生命周期的依赖,一个不希望的例子是创建依赖项成本高昂且并非总是需要。
注入依赖项和使用依赖项并不相同。实际上,在构造函数注入的情况下,构造函数中依赖项的注入通常会导致将传递值的引用存储在某些字段中。依赖项将在稍后调用“父”对象的方时使用。
Func
此方法是最简单的,建议注入 Func
而不是 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
调用 AddFunc
。
IDefer 和 Defer
此方法建议更明确的 API - 注入 IDefer
或 Defer
而不是 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
不同,需要只需要调用一次的 AddDefer
方法。如果你需要协变,请使用 IDefer
接口。
命名工厂
这种方法实现起来最难,但从公共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
接口实现。对于所有未注册的名称(包括null),它将返回DefaultRepository
。
它是如何工作的?
这里很简单。这三个方法都归结为将依赖项解析委派给合适的 IServiceProvider
。什么是合适的?一般来说,在ASP.NET Core应用程序中,人们习惯于使用从IHttpContextAccessor
- HttpContext.RequestServices
获得的一个(作用域)提供商。但在一般情况下,可以有多个这样的提供商。此外,使用依赖项的代码并不知道它们的 existence。这可能是一个通用目的库,它与特定的应用程序环境不是很紧密耦合。因此,引入了获取合适的IServiceProvider
的抽象。是的,又一种抽象!
此项目提供了两个内置提供程序。
AspNetCoreHttpScopeProvider
为ASP.NET Core应用程序。GenericScopeProvider
为通用目的库。
在 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
API的行为:只需使用来自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。
常见问题解答
Q。等等,Microsoft.Extensions.DependencyInjection
不是已经有了这样的内置支持吗?
A。不幸的是没有。我本来也希望能够使用现有功能,而不是编写自己的软件包。
<br/>
Q。你不提供的是一个服务定位器吗?我听说服务定位器是一种反模式。
A。是的,服务定位器是一种已知的反模式。与所提出的解决方案的基本区别在于,服务定位器允许你在运行时解析任何依赖项,而Func
、Defer
和命名工厂仅设计用于在编译时解析指定的只已知依赖项。因此,主要的区别在于类的所有依赖都被显式声明并注入到其中。类自身不会在其实现中秘密地拉取这些依赖。这就是所谓的显式依赖原则。
<br/>
Q。这是否是一种新的依赖注入方法?
A. 实际上不是。关于这种方法的文章可以在许多年前找到,例如这里。
<br/>
Q. 我应该优先选择Func<>
还是[I]Defer<>
?
A. 主要取决于这些API在什么上下文中使用,它们在内部的工作方式是一样的。
主要有两个不同点
Func<>
的优点是,使用Func<>
进行注入的代码不需要任何新的依赖,它是众所周知.NET委托类型。相反,[I]Defer<>
需要引用到SteroidsDI.Core
包。对于你想作为
Func<T>
注入的每个依赖项T
,你应该调用AddFunc
。相反,需要只调用一次AddDefer
方法。
<br/>
Q. 如果我想创建自己的作用域来使用,即不仅仅是消费它,也提供它呢?
A. 首先,你需要以某种方式获取root 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-windows 已计算。 |
.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 已计算。 |
-
.NETCoreApp 3.1
- SteroidsDI.Core (>= 1.4.2)
-
.NETStandard 2.0
- Microsoft.AspNetCore.Http (>= 2.2.2)
- SteroidsDI.Core (>= 1.4.2)
-
net5.0
- SteroidsDI.Core (>= 1.4.2)
-
net6.0
- SteroidsDI.Core (>= 1.4.2)
-
net7.0
- SteroidsDI.Core (>= 1.4.2)
-
net8.0
- SteroidsDI.Core (>= 1.4.2)
NuGet 包
此包未被任何 NuGet 包使用。
GitHub 存储库
此包未被任何流行的 GitHub 存储库使用。
版本 | 下载 | 最后更新 |
---|---|---|
1.4.2 | 2,376 | 3/18/2024 |
1.4.1 | 1,929 | 1/20/2024 |
1.4.0 | 480 | 1/4/2024 |
1.3.0 | 778 | 11/18/2023 |
1.2.0 | 9,226 | 12/29/2022 |
1.1.1 | 2,359 | 12/1/2022 |
1.1.0 | 6,761 | 5/2/2022 |
1.0.3 | 861 | 4/15/2022 |
1.0.2 | 1,267 | 3/31/2022 |
1.0.1 | 7,851 | 10/16/2021 |
1.0.0 | 13,128 | 2/26/2021 |
0.0.7 | 4,336 | 11/24/2020 |
0.0.6 | 774 | 11/24/2020 |
0.0.5 | 1,799 | 10/26/2020 |
0.0.4 | 18,908 | 6/25/2020 |
0.0.3 | 970 | 3/10/2020 |
0.0.2 | 866 | 3/9/2020 |
0.0.1 | 936 | 3/8/2020 |